diff --git a/x/wasm/Governance.md b/x/wasm/Governance.md new file mode 100644 index 00000000..da47240c --- /dev/null +++ b/x/wasm/Governance.md @@ -0,0 +1,205 @@ +# Governance + +This document gives an overview of how the various governance +proposals interact with the CosmWasm contract lifecycle. It is +a high-level, technical introduction meant to provide context before +looking into the code, or constructing proposals. + +## Proposal Types +We have added 9 new wasm specific proposal types that cover the contract's live cycle and authorization: + +* `StoreCodeProposal` - upload a wasm binary +* `InstantiateContractProposal` - instantiate a wasm contract +* `MigrateContractProposal` - migrate a wasm contract to a new code version +* `SudoContractProposal` - call into the protected `sudo` entry point of a contract +* `ExecuteContractProposal` - execute a wasm contract as an arbitrary user +* `UpdateAdminProposal` - set a new admin for a contract +* `ClearAdminProposal` - clear admin for a contract to prevent further migrations +* `PinCodes` - pin the given code ids in cache. This trades memory for reduced startup time and lowers gas cost +* `UnpinCodes` - unpin the given code ids from the cache. This frees up memory and returns to standard speed and gas cost +* `UpdateInstantiateConfigProposal` - update instantiate permissions to a list of given code ids. +* `StoreAndInstantiateContractProposal` - upload and instantiate a wasm contract. + +For details see the proposal type [implementation](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/proposal.go) + +### Unit tests +[Proposal type validations](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/proposal_test.go) + +## Proposal Handler +The [wasmd proposal_handler](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/proposal_handler.go) implements the `gov.Handler` function +and executes the wasmd proposal types after a successful tally. + +The proposal handler uses a [`GovAuthorizationPolicy`](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/authz_policy.go#L29) to bypass the existing contract's authorization policy. + +### Tests +* [Integration: Submit and execute proposal](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/proposal_integration_test.go) + +## Gov Integration +The wasmd proposal handler can be added to the gov router in the [abci app](https://github.com/CosmWasm/wasmd/blob/master/app/app.go#L306) +to receive proposal execution calls. +```go +govRouter.AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.wasmKeeper, enabledProposals)) +``` + +## Wasmd Authorization Settings + +Settings via sdk `params` module: +- `code_upload_access` - who can upload a wasm binary: `Nobody`, `Everybody`, `OnlyAddress` +- `instantiate_default_permission` - platform default, who can instantiate a wasm binary when the code owner has not set it + +See [params.go](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/params.go) + +### Init Params Via Genesis + +```json + "wasm": { + "params": { + "code_upload_access": { + "permission": "Everybody" + }, + "instantiate_default_permission": "Everybody" + } + }, +``` + +The values can be updated via gov proposal implemented in the `params` module. + +### Update Params Via [ParamChangeProposal](https://github.com/cosmos/cosmos-sdk/blob/v0.45.3/proto/cosmos/params/v1beta1/params.proto#L10) +Example to submit a parameter change gov proposal: +```sh +wasmd tx gov submit-proposal param-change --from validator --chain-id=testing -b block +``` +#### Content examples +* Disable wasm code uploads +```json +{ + "title": "Foo", + "description": "Bar", + "changes": [ + { + "subspace": "wasm", + "key": "uploadAccess", + "value": { + "permission": "Nobody" + } + } + ], + "deposit": "" +} +``` +* Allow wasm code uploads for everybody +```json +{ + "title": "Foo", + "description": "Bar", + "changes": [ + { + "subspace": "wasm", + "key": "uploadAccess", + "value": { + "permission": "Everybody" + } + } + ], + "deposit": "" +} +``` + +* Restrict code uploads to a single address +```json +{ + "title": "Foo", + "description": "Bar", + "changes": [ + { + "subspace": "wasm", + "key": "uploadAccess", + "value": { + "permission": "OnlyAddress", + "address": "cosmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0fr2sh" + } + } + ], + "deposit": "" +} +``` +* Set chain **default** instantiation settings to nobody +```json +{ + "title": "Foo", + "description": "Bar", + "changes": [ + { + "subspace": "wasm", + "key": "instantiateAccess", + "value": "Nobody" + } + ], + "deposit": "" +} +``` +* Set chain **default** instantiation settings to everybody +```json +{ + "title": "Foo", + "description": "Bar", + "changes": [ + { + "subspace": "wasm", + "key": "instantiateAccess", + "value": "Everybody" + } + ], + "deposit": "" +} +``` + +### Enable gov proposals at **compile time**. +As gov proposals bypass the existing authorization policy they are disabled and require to be enabled at compile time. +``` +-X github.com/CosmWasm/wasmd/app.ProposalsEnabled=true - enable all x/wasm governance proposals (default false) +-X github.com/CosmWasm/wasmd/app.EnableSpecificProposals=MigrateContract,UpdateAdmin,ClearAdmin - enable a subset of the x/wasm governance proposal types (overrides ProposalsEnabled) +``` + +The `ParamChangeProposal` is always enabled. + +### Tests +* [params validation unit tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/params_test.go) +* [genesis validation tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/types/genesis_test.go) +* [policy integration tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/keeper/keeper_test.go) + +## CLI + +```shell script + wasmd tx gov submit-proposal [command] + +Available Commands: + wasm-store Submit a wasm binary proposal + instantiate-contract Submit an instantiate wasm contract proposal + migrate-contract Submit a migrate wasm contract to a new code version proposal + set-contract-admin Submit a new admin for a contract proposal + clear-contract-admin Submit a clear admin for a contract to prevent further migrations proposal +... +``` +## Rest +New [`ProposalHandlers`](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/client/proposal_handler.go) + +* Integration +```shell script +gov.NewAppModuleBasic(append(wasmclient.ProposalHandlers, paramsclient.ProposalHandler, distr.ProposalHandler, upgradeclient.ProposalHandler)...), +``` +In [abci app](https://github.com/CosmWasm/wasmd/blob/master/app/app.go#L109) + +### Tests +* [Rest Unit tests](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/client/proposal_handler_test.go) +* [Rest smoke LCD test](https://github.com/CosmWasm/wasmd/blob/master/lcd_test/wasm_test.go) + + + +## Pull requests +* https://github.com/CosmWasm/wasmd/pull/190 +* https://github.com/CosmWasm/wasmd/pull/186 +* https://github.com/CosmWasm/wasmd/pull/183 +* https://github.com/CosmWasm/wasmd/pull/180 +* https://github.com/CosmWasm/wasmd/pull/179 +* https://github.com/CosmWasm/wasmd/pull/173 diff --git a/x/wasm/IBC.md b/x/wasm/IBC.md new file mode 100644 index 00000000..c3cd0a00 --- /dev/null +++ b/x/wasm/IBC.md @@ -0,0 +1,137 @@ +# IBC specification + +This documents how CosmWasm contracts are expected to interact with IBC. + +## General Concepts + +**IBC Enabled** - when instantiating a contract, we detect if it supports IBC messages. + We require "feature flags" in the contract/vm handshake to ensure compatibility + for features like staking or chain-specific extensions. IBC functionality will require + another "feature flag", and the list of "enabled features" can be returned to the `x/wasm` + module to control conditional IBC behavior. + + If this feature is enabled, it is considered "IBC Enabled", and that info will + be stored in the ContractInfo. (For mock, we assume all contracts are IBC enabled) + +Also, please read the [IBC Docs](https://docs.cosmos.network/master/ibc/overview.html) +for detailed descriptions of the terms *Port*, *Client*, *Connection*, +and *Channel* + +## Overview + +We use "One Port per Contract", which is the most straight-forward mapping, treating each contract +like a module. It does lead to very long portIDs however. Pay special attention to both the Channel establishment +(which should be compatible with standard ICS20 modules without changes on their part), as well +as how contracts can properly identify their counterparty. + +(We considered on port for the `x/wasm` module and multiplexing on it, but [dismissed that idea](#rejected-ideas)) + +* Upon `Instantiate`, if a contract is *IBC Enabled*, we dynamically + bind a port for this contract. The port name is `wasm.`, + eg. `wasm.cosmos1hmdudppzceg27qsuq707tjg8rkgj7g5hnvnw29` +* If a *Channel* is being established with a registered `wasm.xyz` port, + the `x/wasm.Keeper` will handle this and call into the appropriate + contract to determine supported protocol versions during the + [`ChanOpenTry` and `ChanOpenAck` phases](https://docs.cosmos.network/master/ibc/overview.html#channels). + (See [Channel Handshake Version Negotiation](https://docs.cosmos.network/master/ibc/custom.html#channel-handshake-version-negotiation)) +* Both the *Port* and the *Channel* are fully owned by one contract. +* `x/wasm` will allow both *ORDERED* and *UNORDERED* channels and pass that mode + down to the contract in `OnChanOpenTry`, so the contract can decide if it accepts + the mode. We will recommend the contract developers stick with *ORDERED* channels + for custom protocols unless they can reason about async packet timing. +* When sending a packet, the CosmWasm contract must specify the local *ChannelID*. + As there is a unique *PortID* per contract, that is filled in by `x/wasm` + to produce the globally unique `(PortID, ChannelID)` +* When receiving a Packet (or Ack or Timeout), the contracts receives the local + *ChannelID* it came from, as well as the packet that was sent by the counterparty. +* When receiving an Ack or Timeout packet, the contract also receives the + original packet that it sent earlier. +* We do not support multihop packets in this model (they are rejected by `x/wasm`). + They are currently not fully specified nor implemented in IBC 1.0, so let us + simplify our model until this is well established + +## Workflow + +Establishing *Clients* and *Connections* is out of the scope of this +module and must be created by the same means as for `ibc-transfer` +(via the [go cli](https://github.com/cosmos/relayer) or better [ts-relayer](https://github.com/confio/ts-relayer)). +`x/wasm` will bind a unique *Port* for each "IBC Enabled" contract. + +For mocks, all the Packet Handling and Channel Lifecycle Hooks are routed +to some Golang stub handler, but containing the contract address, so we +can perform contract-specific actions for each packet. In a real setting, +we route to the contract that owns the port/channel and call one of it's various +entry points. + +Please refer to the CosmWasm repo for all +[details on the IBC API from the point of view of a CosmWasm contract](https://github.com/CosmWasm/cosmwasm/blob/main/IBC.md). + +## Future Ideas + +Here are some ideas we may add in the future + +### Dynamic Ports and Channels + +* multiple ports per contract +* elastic ports that can be assigned to different contracts +* transfer of channels to another contract + +This is inspired by the Agoric design, but also adds considerable complexity to both the `x/wasm` +implementation as well as the correctness reasoning of any given contract. This will not be +available in the first version of our "IBC Enabled contracts", but we can consider it for later, +if there are concrete user cases that would significantly benefit from this added complexity. + +### Add multihop support + +Once the ICS and IBC specs fully establish how multihop packets work, we should add support for that. +Both on setting up the routes with OpenChannel, as well as acting as an intermediate relayer (if that is possible) + +## Rejected Ideas + +### One Port per Module + +We decided on "one port per contract", especially after the IBC team raised +the max length on port names to allow `wasm-` to be a valid port. +Here are the arguments for "one port for x/wasm" vs "one port per contract". Here +was an alternate proposal: + +In this approach, the `x/wasm` module just binds one port to handle all +modules. This can be well defined name like `wasm`. Since we always +have `(ChannelID, PortID)` for routing messages, we can reuse one port +for all contracts as long as we have a clear way to map the `ChannelID` +to a specific contract when it is being established. + + +* On genesis we bind the port `wasm` for all communication with the `x/wasm` + module. +* The *Port* is fully owned by `x/wasm` +* Each *Channel* is fully owned by one contract. +* `x/wasm` only accepts *ORDERED Channels* for simplicity of contract + correctness. + +To clarify: + +* When a *Channel* is being established with port `wasm`, the + `x/wasm.Keeper` must be able to identify for which contract this + is destined. **how to do so**?? + * One idea: the channel name must be the contract address. This means + (`wasm`, `cosmos13d...`) will map to the given contract in the wasm module. + The problem with this is that if two contracts from chainA want to + connect to the same contracts on chainB, they will want to claim the + same *ChannelID* and *PortID*. Not sure how to differentiate multiple + parties in this way. + * Other ideas: have a special field we send on `OnChanOpenInit` that + specifies the destination contract, and allow any *ChannelID*. + However, looking at [`OnChanOpenInit` function signature](https://docs.cosmos.network/master/ibc/custom.html#implement-ibcmodule-interface-and-callbacks), + I don't see a place to put this extra info, without abusing the version field, + which is a [specified field](https://docs.cosmos.network/master/ibc/custom.html#channel-handshake-version-negotiation): + ``` + Versions must be strings but can implement any versioning structure. + If your application plans to have linear releases then semantic versioning is recommended. + ... + Valid version selection includes selecting a compatible version identifier with a subset + of features supported by your application for that version. + ... + ICS20 currently implements basic string matching with a + single supported version. + ``` \ No newline at end of file diff --git a/x/wasm/README.md b/x/wasm/README.md new file mode 100644 index 00000000..cba9c5cf --- /dev/null +++ b/x/wasm/README.md @@ -0,0 +1,219 @@ +# Wasm Module + +This should be a brief overview of the functionality + +## Configuration + +You can add the following section to `config/app.toml`: + +```toml +[wasm] +# This is the maximum sdk gas (wasm and storage) that we allow for any x/wasm "smart" queries +query_gas_limit = 300000 +# This defines the memory size for Wasm modules that we can keep cached to speed-up instantiation +# The value is in MiB not bytes +memory_cache_size = 300 +``` + +The values can also be set via CLI flags on with the `start` command: +```shell script +--wasm.memory_cache_size uint32 Sets the size in MiB (NOT bytes) of an in-memory cache for wasm modules. Set to 0 to disable. (default 100) +--wasm.query_gas_limit uint Set the max gas that can be spent on executing a query with a Wasm contract (default 3000000) +``` + +## Events + +A number of events are returned to allow good indexing of the transactions from smart contracts. + +Every call to Instantiate or Execute will be tagged with the info on the contract that was executed and who executed it. +It should look something like this (with different addresses). The module is always `wasm`, and `code_id` is only present +when Instantiating a contract, so you can subscribe to new instances, it is omitted on Execute. There is also an `action` tag +which is auto-added by the Cosmos SDK and has a value of either `store-code`, `instantiate` or `execute` depending on which message +was sent: + +```json +{ + "Type": "message", + "Attr": [ + { + "key": "module", + "value": "wasm" + }, + { + "key": "action", + "value": "instantiate" + }, + { + "key": "signer", + "value": "cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x" + }, + { + "key": "code_id", + "value": "1" + }, + { + "key": "_contract_address", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + } + ] +} +``` + +If any funds were transferred to the contract as part of the message, or if the contract released funds as part of it's executions, +it will receive the typical events associated with sending tokens from bank. In this case, we instantiate the contract and +provide a initial balance in the same `MsgInstantiateContract`. We see the following events in addition to the above one: + +```json +[ + { + "Type": "transfer", + "Attr": [ + { + "key": "recipient", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + }, + { + "key": "sender", + "value": "cosmos1ffnqn02ft2psvyv4dyr56nnv6plllf9pm2kpmv" + }, + { + "key": "amount", + "value": "100000denom" + } + ] + } +] +``` + +Finally, the contract itself can emit a "custom event" on Execute only (not on Init). +There is one event per contract, so if one contract calls a second contract, you may receive +one event for the original contract and one for the re-invoked contract. All attributes from the contract are passed through verbatim, +and we add a `_contract_address` attribute that contains the actual contract that emitted that event. +Here is an example from the escrow contract successfully releasing funds to the destination address: + +```json +{ + "Type": "wasm", + "Attr": [ + { + "key": "_contract_address", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + }, + { + "key": "action", + "value": "release" + }, + { + "key": "destination", + "value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq" + } + ] +} +``` + +### Pulling this all together + +We will invoke an escrow contract to release to the designated beneficiary. +The escrow was previously loaded with `100000denom` (from the above example). +In this transaction, we send `5000denom` along with the `MsgExecuteContract` +and the contract releases the entire funds (`105000denom`) to the beneficiary. + +We will see all the following events, where you should be able to reconstruct the actions +(remember there are two events for each transfer). We see (1) the initial transfer of funds +to the contract, (2) the contract custom event that it released funds (3) the transfer of funds +from the contract to the beneficiary and (4) the generic x/wasm event stating that the contract +was executed (which always appears, while 2 is optional and has information as reliable as the contract): + +```json +[ + { + "Type": "transfer", + "Attr": [ + { + "key": "recipient", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + }, + { + "key": "sender", + "value": "cosmos1zm074khx32hqy20hlshlsd423n07pwlu9cpt37" + }, + { + "key": "amount", + "value": "5000denom" + } + ] + }, + { + "Type": "wasm", + "Attr": [ + { + "key": "_contract_address", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + }, + { + "key": "action", + "value": "release" + }, + { + "key": "destination", + "value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq" + } + ] + }, + { + "Type": "transfer", + "Attr": [ + { + "key": "recipient", + "value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq" + }, + { + "key": "sender", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + }, + { + "key": "amount", + "value": "105000denom" + } + ] + }, + { + "Type": "message", + "Attr": [ + { + "key": "module", + "value": "wasm" + }, + { + "key": "action", + "value": "execute" + }, + { + "key": "signer", + "value": "cosmos1zm074khx32hqy20hlshlsd423n07pwlu9cpt37" + }, + { + "key": "_contract_address", + "value": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + } + ] + } +] +``` + +A note on this format. This is what we return from our module. However, it seems to me that many events with the same `Type` +get merged together somewhere along the stack, so in this case, you *may* end up with one "transfer" event with the info for +both transfers. Double check when evaluating the event logs, I will document better with more experience, especially when I +find out the entire path for the events. + +## Messages + +TODO + +## CLI + +TODO - working, but not the nicest interface (json + bash = bleh). Use to upload, but I suggest to focus on frontend / js tooling + +## Rest + +TODO - main supported interface, under rapid change diff --git a/x/wasm/alias.go b/x/wasm/alias.go new file mode 100644 index 00000000..b7e60d22 --- /dev/null +++ b/x/wasm/alias.go @@ -0,0 +1,133 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/cerc-io/laconicd/x/wasm/types +// ALIASGEN: github.com/cerc-io/laconicd/x/wasm/keeper +package wasm + +import ( + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +const ( + firstCodeID = 1 + ModuleName = types.ModuleName + StoreKey = types.StoreKey + TStoreKey = types.TStoreKey + QuerierRoute = types.QuerierRoute + RouterKey = types.RouterKey + WasmModuleEventType = types.WasmModuleEventType + AttributeKeyContractAddr = types.AttributeKeyContractAddr + ProposalTypeStoreCode = types.ProposalTypeStoreCode + ProposalTypeInstantiateContract = types.ProposalTypeInstantiateContract + ProposalTypeMigrateContract = types.ProposalTypeMigrateContract + ProposalTypeUpdateAdmin = types.ProposalTypeUpdateAdmin + ProposalTypeClearAdmin = types.ProposalTypeClearAdmin + QueryListContractByCode = keeper.QueryListContractByCode + QueryGetContract = keeper.QueryGetContract + QueryGetContractState = keeper.QueryGetContractState + QueryGetCode = keeper.QueryGetCode + QueryListCode = keeper.QueryListCode + QueryMethodContractStateSmart = keeper.QueryMethodContractStateSmart + QueryMethodContractStateAll = keeper.QueryMethodContractStateAll + QueryMethodContractStateRaw = keeper.QueryMethodContractStateRaw +) + +var ( + // functions aliases + RegisterCodec = types.RegisterLegacyAminoCodec + RegisterInterfaces = types.RegisterInterfaces + ValidateGenesis = types.ValidateGenesis + ConvertToProposals = types.ConvertToProposals + GetCodeKey = types.GetCodeKey + GetContractAddressKey = types.GetContractAddressKey + GetContractStorePrefixKey = types.GetContractStorePrefix + NewCodeInfo = types.NewCodeInfo + NewAbsoluteTxPosition = types.NewAbsoluteTxPosition + NewContractInfo = types.NewContractInfo + NewEnv = types.NewEnv + NewWasmCoins = types.NewWasmCoins + DefaultWasmConfig = types.DefaultWasmConfig + DefaultParams = types.DefaultParams + InitGenesis = keeper.InitGenesis + ExportGenesis = keeper.ExportGenesis + NewMessageHandler = keeper.NewDefaultMessageHandler + DefaultEncoders = keeper.DefaultEncoders + EncodeBankMsg = keeper.EncodeBankMsg + NoCustomMsg = keeper.NoCustomMsg + EncodeStakingMsg = keeper.EncodeStakingMsg + EncodeWasmMsg = keeper.EncodeWasmMsg + NewKeeper = keeper.NewKeeper + DefaultQueryPlugins = keeper.DefaultQueryPlugins + BankQuerier = keeper.BankQuerier + NoCustomQuerier = keeper.NoCustomQuerier + StakingQuerier = keeper.StakingQuerier + WasmQuerier = keeper.WasmQuerier + CreateTestInput = keeper.CreateTestInput + TestHandler = keeper.TestHandler + NewWasmProposalHandler = keeper.NewWasmProposalHandler + NewQuerier = keeper.Querier + ContractFromPortID = keeper.ContractFromPortID + WithWasmEngine = keeper.WithWasmEngine + NewCountTXDecorator = keeper.NewCountTXDecorator + + // variable aliases + ModuleCdc = types.ModuleCdc + DefaultCodespace = types.DefaultCodespace + ErrCreateFailed = types.ErrCreateFailed + ErrAccountExists = types.ErrAccountExists + ErrInstantiateFailed = types.ErrInstantiateFailed + ErrExecuteFailed = types.ErrExecuteFailed + ErrGasLimit = types.ErrGasLimit + ErrInvalidGenesis = types.ErrInvalidGenesis + ErrNotFound = types.ErrNotFound + ErrQueryFailed = types.ErrQueryFailed + ErrInvalidMsg = types.ErrInvalidMsg + KeyLastCodeID = types.KeyLastCodeID + KeyLastInstanceID = types.KeyLastInstanceID + CodeKeyPrefix = types.CodeKeyPrefix + ContractKeyPrefix = types.ContractKeyPrefix + ContractStorePrefix = types.ContractStorePrefix + EnableAllProposals = types.EnableAllProposals + DisableAllProposals = types.DisableAllProposals +) + +type ( + ProposalType = types.ProposalType + GenesisState = types.GenesisState + Code = types.Code + Contract = types.Contract + MsgStoreCode = types.MsgStoreCode + MsgStoreCodeResponse = types.MsgStoreCodeResponse + MsgInstantiateContract = types.MsgInstantiateContract + MsgInstantiateContract2 = types.MsgInstantiateContract2 + MsgInstantiateContractResponse = types.MsgInstantiateContractResponse + MsgExecuteContract = types.MsgExecuteContract + MsgExecuteContractResponse = types.MsgExecuteContractResponse + MsgMigrateContract = types.MsgMigrateContract + MsgMigrateContractResponse = types.MsgMigrateContractResponse + MsgUpdateAdmin = types.MsgUpdateAdmin + MsgUpdateAdminResponse = types.MsgUpdateAdminResponse + MsgClearAdmin = types.MsgClearAdmin + MsgWasmIBCCall = types.MsgIBCSend + MsgClearAdminResponse = types.MsgClearAdminResponse + MsgServer = types.MsgServer + Model = types.Model + CodeInfo = types.CodeInfo + ContractInfo = types.ContractInfo + CreatedAt = types.AbsoluteTxPosition + Config = types.WasmConfig + CodeInfoResponse = types.CodeInfoResponse + MessageHandler = keeper.SDKMessageHandler + BankEncoder = keeper.BankEncoder + CustomEncoder = keeper.CustomEncoder + StakingEncoder = keeper.StakingEncoder + WasmEncoder = keeper.WasmEncoder + MessageEncoders = keeper.MessageEncoders + Keeper = keeper.Keeper + QueryHandler = keeper.QueryHandler + CustomQuerier = keeper.CustomQuerier + QueryPlugins = keeper.QueryPlugins + Option = keeper.Option +) diff --git a/x/wasm/client/cli/gov_tx.go b/x/wasm/client/cli/gov_tx.go new file mode 100644 index 00000000..4cff5963 --- /dev/null +++ b/x/wasm/client/cli/gov_tx.go @@ -0,0 +1,834 @@ +package cli + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/docker/distribution/reference" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/pkg/errors" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func ProposalStoreCodeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "wasm-store [wasm file] --title [text] --description [text] --run-as [address] --unpin-code [unpin_code] --source [source] --builder [builder] --code-hash [code_hash]", + Short: "Submit a wasm binary proposal", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + src, err := parseStoreCodeArgs(args[0], clientCtx.FromAddress, cmd.Flags()) + if err != nil { + return err + } + runAs, err := cmd.Flags().GetString(flagRunAs) + if err != nil { + return fmt.Errorf("run-as: %s", err) + } + if len(runAs) == 0 { + return errors.New("run-as address is required") + } + + unpinCode, err := cmd.Flags().GetBool(flagUnpinCode) + if err != nil { + return err + } + + source, builder, codeHash, err := parseVerificationFlags(src.WASMByteCode, cmd.Flags()) + if err != nil { + return err + } + content := types.StoreCodeProposal{ + Title: proposalTitle, + Description: proposalDescr, + RunAs: runAs, + WASMByteCode: src.WASMByteCode, + InstantiatePermission: src.InstantiatePermission, + UnpinCode: unpinCode, + Source: source, + Builder: builder, + CodeHash: codeHash, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + cmd.Flags().String(flagRunAs, "", "The address that is stored as code creator") + cmd.Flags().Bool(flagUnpinCode, false, "Unpin code on upload, optional") + cmd.Flags().String(flagSource, "", "Code Source URL is a valid absolute HTTPS URI to the contract's source code,") + cmd.Flags().String(flagBuilder, "", "Builder is a valid docker image name with tag, such as \"cosmwasm/workspace-optimizer:0.12.9\"") + cmd.Flags().BytesHex(flagCodeHash, nil, "CodeHash is the sha256 hash of the wasm code") + addInstantiatePermissionFlags(cmd) + + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func parseVerificationFlags(wasm []byte, flags *flag.FlagSet) (string, string, []byte, error) { + source, err := flags.GetString(flagSource) + if err != nil { + return "", "", nil, fmt.Errorf("source: %s", err) + } + builder, err := flags.GetString(flagBuilder) + if err != nil { + return "", "", nil, fmt.Errorf("builder: %s", err) + } + codeHash, err := flags.GetBytesHex(flagCodeHash) + if err != nil { + return "", "", nil, fmt.Errorf("codeHash: %s", err) + } + + // if any set require others to be set + if len(source) != 0 || len(builder) != 0 || len(codeHash) != 0 { + if source == "" { + return "", "", nil, fmt.Errorf("source is required") + } + if _, err = url.ParseRequestURI(source); err != nil { + return "", "", nil, fmt.Errorf("source: %s", err) + } + if builder == "" { + return "", "", nil, fmt.Errorf("builder is required") + } + if _, err := reference.ParseDockerRef(builder); err != nil { + return "", "", nil, fmt.Errorf("builder: %s", err) + } + if len(codeHash) == 0 { + return "", "", nil, fmt.Errorf("code hash is required") + } + // wasm is unzipped in parseStoreCodeArgs + // checksum generation will be decoupled here + // reference https://github.com/CosmWasm/wasmvm/issues/359 + checksum := sha256.Sum256(wasm) + if !bytes.Equal(checksum[:], codeHash) { + return "", "", nil, fmt.Errorf("code-hash mismatch: %X, checksum: %X", codeHash, checksum) + } + } + return source, builder, codeHash, nil +} + +func ProposalInstantiateContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "instantiate-contract [code_id_int64] [json_encoded_init_args] --label [text] --title [text] --description [text] --run-as [address] --admin [address,optional] --amount [coins,optional]", + Short: "Submit an instantiate wasm contract proposal", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + src, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.FromAddress, cmd.Flags()) + if err != nil { + return err + } + + runAs, err := cmd.Flags().GetString(flagRunAs) + if err != nil { + return fmt.Errorf("run-as: %s", err) + } + if len(runAs) == 0 { + return errors.New("run-as address is required") + } + + content := types.InstantiateContractProposal{ + Title: proposalTitle, + Description: proposalDescr, + RunAs: runAs, + Admin: src.Admin, + CodeID: src.CodeID, + Label: src.Label, + Msg: src.Msg, + Funds: src.Funds, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") + cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists") + cmd.Flags().String(flagAdmin, "", "Address or key name of an admin") + cmd.Flags().String(flagRunAs, "", "The address that pays the init funds. It is the creator of the contract and passed to the contract as sender on proposal execution") + cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin") + + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalInstantiateContract2Cmd() *cobra.Command { + decoder := newArgDecoder(hex.DecodeString) + cmd := &cobra.Command{ + Use: "instantiate-contract-2 [code_id_int64] [json_encoded_init_args] [salt] --label [text] --title [text] --description [text] --run-as [address] --admin [address,optional] --amount [coins,optional] --fix-msg [bool,optional]", + Short: "Submit an instantiate wasm contract proposal with predictable address", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + src, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.FromAddress, cmd.Flags()) + if err != nil { + return err + } + + runAs, err := cmd.Flags().GetString(flagRunAs) + if err != nil { + return fmt.Errorf("run-as: %s", err) + } + if len(runAs) == 0 { + return errors.New("run-as address is required") + } + + salt, err := decoder.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("salt: %w", err) + } + + fixMsg, err := cmd.Flags().GetBool(flagFixMsg) + if err != nil { + return fmt.Errorf("fix msg: %w", err) + } + + content := types.NewInstantiateContract2Proposal(proposalTitle, proposalDescr, runAs, src.Admin, src.CodeID, src.Label, src.Msg, src.Funds, salt, fixMsg) + + msg, err := govtypes.NewMsgSubmitProposal(content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") + cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists") + cmd.Flags().String(flagAdmin, "", "Address of an admin") + cmd.Flags().String(flagRunAs, "", "The address that pays the init funds. It is the creator of the contract and passed to the contract as sender on proposal execution") + cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin") + cmd.Flags().Bool(flagFixMsg, false, "An optional flag to include the json_encoded_init_args for the predictable address generation mode") + decoder.RegisterFlags(cmd.PersistentFlags(), "salt") + + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalStoreAndInstantiateContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "store-instantiate [wasm file] [json_encoded_init_args] --label [text] --title [text] --description [text] --run-as [address]" + + "--unpin-code [unpin_code,optional] --source [source,optional] --builder [builder,optional] --code-hash [code_hash,optional] --admin [address,optional] --amount [coins,optional]", + Short: "Submit and instantiate a wasm contract proposal", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + src, err := parseStoreCodeArgs(args[0], clientCtx.FromAddress, cmd.Flags()) + if err != nil { + return err + } + runAs, err := cmd.Flags().GetString(flagRunAs) + if err != nil { + return fmt.Errorf("run-as: %s", err) + } + if len(runAs) == 0 { + return errors.New("run-as address is required") + } + + unpinCode, err := cmd.Flags().GetBool(flagUnpinCode) + if err != nil { + return err + } + + source, builder, codeHash, err := parseVerificationFlags(src.WASMByteCode, cmd.Flags()) + if err != nil { + return err + } + + amountStr, err := cmd.Flags().GetString(flagAmount) + if err != nil { + return fmt.Errorf("amount: %s", err) + } + amount, err := sdk.ParseCoinsNormalized(amountStr) + if err != nil { + return fmt.Errorf("amount: %s", err) + } + label, err := cmd.Flags().GetString(flagLabel) + if err != nil { + return fmt.Errorf("label: %s", err) + } + if label == "" { + return errors.New("label is required on all contracts") + } + adminStr, err := cmd.Flags().GetString(flagAdmin) + if err != nil { + return fmt.Errorf("admin: %s", err) + } + noAdmin, err := cmd.Flags().GetBool(flagNoAdmin) + if err != nil { + return fmt.Errorf("no-admin: %s", err) + } + + // ensure sensible admin is set (or explicitly immutable) + if adminStr == "" && !noAdmin { + return fmt.Errorf("you must set an admin or explicitly pass --no-admin to make it immutible (wasmd issue #719)") + } + if adminStr != "" && noAdmin { + return fmt.Errorf("you set an admin and passed --no-admin, those cannot both be true") + } + + if adminStr != "" { + addr, err := sdk.AccAddressFromBech32(adminStr) + if err != nil { + info, err := clientCtx.Keyring.Key(adminStr) + if err != nil { + return fmt.Errorf("admin %s", err) + } + adminStr = info.GetAddress().String() + } else { + adminStr = addr.String() + } + } + + content := types.StoreAndInstantiateContractProposal{ + Title: proposalTitle, + Description: proposalDescr, + RunAs: runAs, + WASMByteCode: src.WASMByteCode, + InstantiatePermission: src.InstantiatePermission, + UnpinCode: unpinCode, + Source: source, + Builder: builder, + CodeHash: codeHash, + Admin: adminStr, + Label: label, + Msg: []byte(args[1]), + Funds: amount, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + cmd.Flags().String(flagRunAs, "", "The address that is stored as code creator. It is the creator of the contract and passed to the contract as sender on proposal execution") + cmd.Flags().Bool(flagUnpinCode, false, "Unpin code on upload, optional") + cmd.Flags().String(flagSource, "", "Code Source URL is a valid absolute HTTPS URI to the contract's source code,") + cmd.Flags().String(flagBuilder, "", "Builder is a valid docker image name with tag, such as \"cosmwasm/workspace-optimizer:0.12.9\"") + cmd.Flags().BytesHex(flagCodeHash, nil, "CodeHash is the sha256 hash of the wasm code") + cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") + cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists") + cmd.Flags().String(flagAdmin, "", "Address or key name of an admin") + cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin") + addInstantiatePermissionFlags(cmd) + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalMigrateContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "migrate-contract [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args]", + Short: "Submit a migrate wasm contract to a new code version proposal", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + src, err := parseMigrateContractArgs(args, clientCtx) + if err != nil { + return err + } + + content := types.MigrateContractProposal{ + Title: proposalTitle, + Description: proposalDescr, + Contract: src.Contract, + CodeID: src.CodeID, + Msg: src.Msg, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalExecuteContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "execute-contract [contract_addr_bech32] [json_encoded_migration_args]", + Short: "Submit a execute wasm contract proposal (run by any address)", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + contract := args[0] + execMsg := []byte(args[1]) + amountStr, err := cmd.Flags().GetString(flagAmount) + if err != nil { + return fmt.Errorf("amount: %s", err) + } + funds, err := sdk.ParseCoinsNormalized(amountStr) + if err != nil { + return fmt.Errorf("amount: %s", err) + } + runAs, err := cmd.Flags().GetString(flagRunAs) + if err != nil { + return fmt.Errorf("run-as: %s", err) + } + + if len(runAs) == 0 { + return errors.New("run-as address is required") + } + + content := types.ExecuteContractProposal{ + Title: proposalTitle, + Description: proposalDescr, + Contract: contract, + Msg: execMsg, + RunAs: runAs, + Funds: funds, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + cmd.Flags().String(flagRunAs, "", "The address that is passed as sender to the contract on proposal execution") + cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") + + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalSudoContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "sudo-contract [contract_addr_bech32] [json_encoded_migration_args]", + Short: "Submit a sudo wasm contract proposal (to call privileged commands)", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + contract := args[0] + sudoMsg := []byte(args[1]) + + content := types.SudoContractProposal{ + Title: proposalTitle, + Description: proposalDescr, + Contract: contract, + Msg: sudoMsg, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + // proposal flagsExecute + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalUpdateContractAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32]", + Short: "Submit a new admin for a contract proposal", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + src, err := parseUpdateContractAdminArgs(args, clientCtx) + if err != nil { + return err + } + + content := types.UpdateAdminProposal{ + Title: proposalTitle, + Description: proposalDescr, + Contract: src.Contract, + NewAdmin: src.NewAdmin, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalClearContractAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "clear-contract-admin [contract_addr_bech32]", + Short: "Submit a clear admin for a contract to prevent further migrations proposal", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + content := types.ClearAdminProposal{ + Title: proposalTitle, + Description: proposalDescr, + Contract: args[0], + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func ProposalPinCodesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "pin-codes [code-ids]", + Short: "Submit a pin code proposal for pinning a code to cache", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + codeIds, err := parsePinCodesArgs(args) + if err != nil { + return err + } + + content := types.PinCodesProposal{ + Title: proposalTitle, + Description: proposalDescr, + CodeIDs: codeIds, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func parsePinCodesArgs(args []string) ([]uint64, error) { + codeIDs := make([]uint64, len(args)) + for i, c := range args { + codeID, err := strconv.ParseUint(c, 10, 64) + if err != nil { + return codeIDs, fmt.Errorf("code IDs: %s", err) + } + codeIDs[i] = codeID + } + return codeIDs, nil +} + +func ProposalUnpinCodesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "unpin-codes [code-ids]", + Short: "Submit a unpin code proposal for unpinning a code to cache", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + + codeIds, err := parsePinCodesArgs(args) + if err != nil { + return err + } + + content := types.UnpinCodesProposal{ + Title: proposalTitle, + Description: proposalDescr, + CodeIDs: codeIds, + } + + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func parseAccessConfig(raw string) (c types.AccessConfig, err error) { + switch raw { + case "nobody": + return types.AllowNobody, nil + case "everybody": + return types.AllowEverybody, nil + default: + parts := strings.Split(raw, ",") + addrs := make([]sdk.AccAddress, len(parts)) + for i, v := range parts { + addr, err := sdk.AccAddressFromBech32(v) + if err != nil { + return types.AccessConfig{}, fmt.Errorf("unable to parse address %q: %s", v, err) + } + addrs[i] = addr + } + defer func() { // convert panic in ".With" to error for better output + if r := recover(); r != nil { + err = r.(error) + } + }() + cfg := types.AccessTypeAnyOfAddresses.With(addrs...) + return cfg, cfg.ValidateBasic() + } +} + +func parseAccessConfigUpdates(args []string) ([]types.AccessConfigUpdate, error) { + updates := make([]types.AccessConfigUpdate, len(args)) + for i, c := range args { + // format: code_id:access_config + // access_config: nobody|everybody|address(es) + parts := strings.Split(c, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format") + } + + codeID, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid code ID: %s", err) + } + + accessConfig, err := parseAccessConfig(parts[1]) + if err != nil { + return nil, err + } + updates[i] = types.AccessConfigUpdate{ + CodeID: codeID, + InstantiatePermission: accessConfig, + } + } + return updates, nil +} + +func ProposalUpdateInstantiateConfigCmd() *cobra.Command { + bech32Prefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + cmd := &cobra.Command{ + Use: "update-instantiate-config [code-id:permission]...", + Short: "Submit an update instantiate config proposal.", + Args: cobra.MinimumNArgs(1), + Long: strings.TrimSpace( + fmt.Sprintf(`Submit an update instantiate config proposal for multiple code ids. + +Example: +$ %s tx gov submit-proposal update-instantiate-config 1:nobody 2:everybody 3:%s1l2rsakp388kuv9k8qzq6lrm9taddae7fpx59wm,%s1vx8knpllrj7n963p9ttd80w47kpacrhuts497x +`, version.AppName, bech32Prefix, bech32Prefix)), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, proposalDescr, deposit, err := getProposalInfo(cmd) + if err != nil { + return err + } + updates, err := parseAccessConfigUpdates(args) + if err != nil { + return err + } + + content := types.UpdateInstantiateConfigProposal{ + Title: proposalTitle, + Description: proposalDescr, + AccessConfigUpdates: updates, + } + msg, err := govtypes.NewMsgSubmitProposal(&content, deposit, clientCtx.GetFromAddress()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + // proposal flags + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "Description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + return cmd +} + +func getProposalInfo(cmd *cobra.Command) (client.Context, string, string, sdk.Coins, error) { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return client.Context{}, "", "", nil, err + } + + proposalTitle, err := cmd.Flags().GetString(cli.FlagTitle) + if err != nil { + return clientCtx, proposalTitle, "", nil, err + } + + proposalDescr, err := cmd.Flags().GetString(cli.FlagDescription) + if err != nil { + return client.Context{}, proposalTitle, proposalDescr, nil, err + } + + depositArg, err := cmd.Flags().GetString(cli.FlagDeposit) + if err != nil { + return client.Context{}, proposalTitle, proposalDescr, nil, err + } + + deposit, err := sdk.ParseCoinsNormalized(depositArg) + if err != nil { + return client.Context{}, proposalTitle, proposalDescr, deposit, err + } + + return clientCtx, proposalTitle, proposalDescr, deposit, nil +} diff --git a/x/wasm/client/cli/gov_tx_test.go b/x/wasm/client/cli/gov_tx_test.go new file mode 100644 index 00000000..672e203c --- /dev/null +++ b/x/wasm/client/cli/gov_tx_test.go @@ -0,0 +1,158 @@ +package cli + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestParseAccessConfigUpdates(t *testing.T) { + specs := map[string]struct { + src []string + exp []types.AccessConfigUpdate + expErr bool + }{ + "nobody": { + src: []string{"1:nobody"}, + exp: []types.AccessConfigUpdate{{ + CodeID: 1, + InstantiatePermission: types.AccessConfig{Permission: types.AccessTypeNobody}, + }}, + }, + "everybody": { + src: []string{"1:everybody"}, + exp: []types.AccessConfigUpdate{{ + CodeID: 1, + InstantiatePermission: types.AccessConfig{Permission: types.AccessTypeEverybody}, + }}, + }, + "any of addresses - single": { + src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"}, + exp: []types.AccessConfigUpdate{ + { + CodeID: 1, + InstantiatePermission: types.AccessConfig{ + Permission: types.AccessTypeAnyOfAddresses, + Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"}, + }, + }, + }, + }, + "any of addresses - multiple": { + src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"}, + exp: []types.AccessConfigUpdate{ + { + CodeID: 1, + InstantiatePermission: types.AccessConfig{ + Permission: types.AccessTypeAnyOfAddresses, + Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x", "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"}, + }, + }, + }, + }, + "multiple code ids with different permissions": { + src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", "2:nobody"}, + exp: []types.AccessConfigUpdate{ + { + CodeID: 1, + InstantiatePermission: types.AccessConfig{ + Permission: types.AccessTypeAnyOfAddresses, + Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x", "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"}, + }, + }, { + CodeID: 2, + InstantiatePermission: types.AccessConfig{ + Permission: types.AccessTypeNobody, + }, + }, + }, + }, + "any of addresses - empty list": { + src: []string{"1:"}, + expErr: true, + }, + "any of addresses - invalid address": { + src: []string{"1:foo"}, + expErr: true, + }, + "any of addresses - duplicate address": { + src: []string{"1:cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := parseAccessConfigUpdates(spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestParseCodeInfoFlags(t *testing.T) { + correctSource := "https://github.com/CosmWasm/wasmd/blob/main/x/wasm/keeper/testdata/hackatom.wasm" + correctBuilderRef := "cosmwasm/workspace-optimizer:0.12.9" + + wasmBin, err := os.ReadFile("../../keeper/testdata/hackatom.wasm") + require.NoError(t, err) + + checksumStr := "beb3de5e9b93b52e514c74ce87ccddb594b9bcd33b7f1af1bb6da63fc883917b" + + specs := map[string]struct { + args []string + expErr bool + }{ + "source missing": { + args: []string{"--builder=" + correctBuilderRef, "--code-hash=" + checksumStr}, + expErr: true, + }, + "builder missing": { + args: []string{"--code-source-url=" + correctSource, "--code-hash=" + checksumStr}, + expErr: true, + }, + "code hash missing": { + args: []string{"--code-source-url=" + correctSource, "--builder=" + correctBuilderRef}, + expErr: true, + }, + "source format wrong": { + args: []string{"--code-source-url=" + "format_wrong", "--builder=" + correctBuilderRef, "--code-hash=" + checksumStr}, + expErr: true, + }, + "builder format wrong": { + args: []string{"--code-source-url=" + correctSource, "--builder=" + "format//", "--code-hash=" + checksumStr}, + expErr: true, + }, + "code hash wrong": { + args: []string{"--code-source-url=" + correctSource, "--builder=" + correctBuilderRef, "--code-hash=" + "AA"}, + expErr: true, + }, + "happy path, none set": { + args: []string{}, + expErr: false, + }, + "happy path all set": { + args: []string{"--code-source-url=" + correctSource, "--builder=" + correctBuilderRef, "--code-hash=" + checksumStr}, + expErr: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + flags := ProposalStoreCodeCmd().Flags() + require.NoError(t, flags.Parse(spec.args)) + _, _, _, gotErr := parseVerificationFlags(wasmBin, flags) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} diff --git a/x/wasm/client/cli/new_tx.go b/x/wasm/client/cli/new_tx.go new file mode 100644 index 00000000..0c742de6 --- /dev/null +++ b/x/wasm/client/cli/new_tx.go @@ -0,0 +1,163 @@ +package cli + +import ( + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/spf13/cobra" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// MigrateContractCmd will migrate a contract to a new code version +func MigrateContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "migrate [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args]", + Short: "Migrate a wasm contract to a new code version", + Aliases: []string{"update", "mig", "m"}, + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg, err := parseMigrateContractArgs(args, clientCtx) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return nil + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + SilenceUsage: true, + } + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func parseMigrateContractArgs(args []string, cliCtx client.Context) (types.MsgMigrateContract, error) { + // get the id of the code to instantiate + codeID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return types.MsgMigrateContract{}, sdkerrors.Wrap(err, "code id") + } + + migrateMsg := args[2] + + msg := types.MsgMigrateContract{ + Sender: cliCtx.GetFromAddress().String(), + Contract: args[0], + CodeID: codeID, + Msg: []byte(migrateMsg), + } + return msg, nil +} + +// UpdateContractAdminCmd sets an new admin for a contract +func UpdateContractAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32]", + Short: "Set new admin for a contract", + Aliases: []string{"new-admin", "admin", "set-adm", "sa"}, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg, err := parseUpdateContractAdminArgs(args, clientCtx) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + SilenceUsage: true, + } + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func parseUpdateContractAdminArgs(args []string, cliCtx client.Context) (types.MsgUpdateAdmin, error) { + msg := types.MsgUpdateAdmin{ + Sender: cliCtx.GetFromAddress().String(), + Contract: args[0], + NewAdmin: args[1], + } + return msg, nil +} + +// ClearContractAdminCmd clears an admin for a contract +func ClearContractAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "clear-contract-admin [contract_addr_bech32]", + Short: "Clears admin for a contract to prevent further migrations", + Aliases: []string{"clear-admin", "clr-adm"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := types.MsgClearAdmin{ + Sender: clientCtx.GetFromAddress().String(), + Contract: args[0], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + SilenceUsage: true, + } + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +// UpdateInstantiateConfigCmd updates instantiate config for a smart contract. +func UpdateInstantiateConfigCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-instantiate-config [code_id_int64]", + Short: "Update instantiate config for a codeID", + Aliases: []string{"update-instantiate-config"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + perm, err := parseAccessConfigFlags(cmd.Flags()) + if err != nil { + return err + } + + msg := types.MsgUpdateInstantiateConfig{ + Sender: string(clientCtx.GetFromAddress()), + CodeID: codeID, + NewInstantiatePermission: perm, + } + if err = msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + SilenceUsage: true, + } + + addInstantiatePermissionFlags(cmd) + flags.AddTxFlagsToCmd(cmd) + return cmd +} diff --git a/x/wasm/client/cli/query.go b/x/wasm/client/cli/query.go new file mode 100644 index 00000000..55b4694a --- /dev/null +++ b/x/wasm/client/cli/query.go @@ -0,0 +1,674 @@ +package cli + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + + wasmvm "github.com/CosmWasm/wasmvm" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func GetQueryCmd() *cobra.Command { + queryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the wasm module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + SilenceUsage: true, + } + queryCmd.AddCommand( + GetCmdListCode(), + GetCmdListContractByCode(), + GetCmdQueryCode(), + GetCmdQueryCodeInfo(), + GetCmdGetContractInfo(), + GetCmdGetContractHistory(), + GetCmdGetContractState(), + GetCmdListPinnedCode(), + GetCmdLibVersion(), + GetCmdQueryParams(), + GetCmdBuildAddress(), + GetCmdListContractsByCreator(), + ) + return queryCmd +} + +// GetCmdLibVersion gets current libwasmvm version. +func GetCmdLibVersion() *cobra.Command { + cmd := &cobra.Command{ + Use: "libwasmvm-version", + Short: "Get libwasmvm version", + Long: "Get libwasmvm version", + Aliases: []string{"lib-version"}, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + version, err := wasmvm.LibwasmvmVersion() + if err != nil { + return fmt.Errorf("error retrieving libwasmvm version: %w", err) + } + fmt.Println(version) + return nil + }, + SilenceUsage: true, + } + return cmd +} + +// GetCmdBuildAddress build a contract address +func GetCmdBuildAddress() *cobra.Command { + decoder := newArgDecoder(hex.DecodeString) + cmd := &cobra.Command{ + Use: "build-address [code-hash] [creator-address] [salt-hex-encoded] [json_encoded_init_args (required when set as fixed)]", + Short: "build contract address", + Aliases: []string{"address"}, + Args: cobra.RangeArgs(3, 4), + RunE: func(cmd *cobra.Command, args []string) error { + codeHash, err := hex.DecodeString(args[0]) + if err != nil { + return fmt.Errorf("code-hash: %s", err) + } + creator, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return fmt.Errorf("creator: %s", err) + } + salt, err := hex.DecodeString(args[2]) + switch { + case err != nil: + return fmt.Errorf("salt: %s", err) + case len(salt) == 0: + return errors.New("empty salt") + } + + if len(args) == 3 { + cmd.Println(keeper.BuildContractAddressPredictable(codeHash, creator, salt, []byte{}).String()) + return nil + } + msg := types.RawContractMessage(args[3]) + if err := msg.ValidateBasic(); err != nil { + return fmt.Errorf("init message: %s", err) + } + cmd.Println(keeper.BuildContractAddressPredictable(codeHash, creator, salt, msg).String()) + return nil + }, + SilenceUsage: true, + } + decoder.RegisterFlags(cmd.PersistentFlags(), "salt") + return cmd +} + +// GetCmdListCode lists all wasm code uploaded +func GetCmdListCode() *cobra.Command { + cmd := &cobra.Command{ + Use: "list-code", + Short: "List all wasm bytecode on the chain", + Long: "List all wasm bytecode on the chain", + Aliases: []string{"list-codes", "codes", "lco"}, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags())) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.Codes( + context.Background(), + &types.QueryCodesRequest{ + Pagination: pageReq, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "list codes") + return cmd +} + +// GetCmdListContractByCode lists all wasm code uploaded for given code id +func GetCmdListContractByCode() *cobra.Command { + cmd := &cobra.Command{ + Use: "list-contract-by-code [code_id]", + Short: "List wasm all bytecode on the chain for given code id", + Long: "List wasm all bytecode on the chain for given code id", + Aliases: []string{"list-contracts-by-code", "list-contracts", "contracts", "lca"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + if codeID == 0 { + return errors.New("empty code id") + } + + pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags())) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.ContractsByCode( + context.Background(), + &types.QueryContractsByCodeRequest{ + CodeId: codeID, + Pagination: pageReq, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "list contracts by code") + return cmd +} + +// GetCmdQueryCode returns the bytecode for a given contract +func GetCmdQueryCode() *cobra.Command { + cmd := &cobra.Command{ + Use: "code [code_id] [output filename]", + Short: "Downloads wasm bytecode for given code id", + Long: "Downloads wasm bytecode for given code id", + Aliases: []string{"source-code", "source"}, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.Code( + context.Background(), + &types.QueryCodeRequest{ + CodeId: codeID, + }, + ) + if err != nil { + return err + } + if len(res.Data) == 0 { + return fmt.Errorf("contract not found") + } + + fmt.Printf("Downloading wasm code to %s\n", args[1]) + return os.WriteFile(args[1], res.Data, 0o600) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +// GetCmdQueryCodeInfo returns the code info for a given code id +func GetCmdQueryCodeInfo() *cobra.Command { + cmd := &cobra.Command{ + Use: "code-info [code_id]", + Short: "Prints out metadata of a code id", + Long: "Prints out metadata of a code id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.Code( + context.Background(), + &types.QueryCodeRequest{ + CodeId: codeID, + }, + ) + if err != nil { + return err + } + if res.CodeInfoResponse == nil { + return fmt.Errorf("contract not found") + } + + return clientCtx.PrintProto(res.CodeInfoResponse) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +// GetCmdGetContractInfo gets details about a given contract +func GetCmdGetContractInfo() *cobra.Command { + cmd := &cobra.Command{ + Use: "contract [bech32_address]", + Short: "Prints out metadata of a contract given its address", + Long: "Prints out metadata of a contract given its address", + Aliases: []string{"meta", "c"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + _, err = sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.ContractInfo( + context.Background(), + &types.QueryContractInfoRequest{ + Address: args[0], + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +// GetCmdGetContractState dumps full internal state of a given contract +func GetCmdGetContractState() *cobra.Command { + cmd := &cobra.Command{ + Use: "contract-state", + Short: "Querying commands for the wasm module", + Aliases: []string{"state", "cs", "s"}, + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + SilenceUsage: true, + } + cmd.AddCommand( + GetCmdGetContractStateAll(), + GetCmdGetContractStateRaw(), + GetCmdGetContractStateSmart(), + ) + return cmd +} + +func GetCmdGetContractStateAll() *cobra.Command { + cmd := &cobra.Command{ + Use: "all [bech32_address]", + Short: "Prints out all internal state of a contract given its address", + Long: "Prints out all internal state of a contract given its address", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + _, err = sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags())) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.AllContractState( + context.Background(), + &types.QueryAllContractStateRequest{ + Address: args[0], + Pagination: pageReq, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "contract state") + return cmd +} + +func GetCmdGetContractStateRaw() *cobra.Command { + decoder := newArgDecoder(hex.DecodeString) + cmd := &cobra.Command{ + Use: "raw [bech32_address] [key]", + Short: "Prints out internal state for key of a contract given its address", + Long: "Prints out internal state for of a contract given its address", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + _, err = sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + queryData, err := decoder.DecodeString(args[1]) + if err != nil { + return err + } + + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.RawContractState( + context.Background(), + &types.QueryRawContractStateRequest{ + Address: args[0], + QueryData: queryData, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + decoder.RegisterFlags(cmd.PersistentFlags(), "key argument") + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func GetCmdGetContractStateSmart() *cobra.Command { + decoder := newArgDecoder(asciiDecodeString) + cmd := &cobra.Command{ + Use: "smart [bech32_address] [query]", + Short: "Calls contract with given address with query data and prints the returned result", + Long: "Calls contract with given address with query data and prints the returned result", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + _, err = sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + if args[1] == "" { + return errors.New("query data must not be empty") + } + + queryData, err := decoder.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("decode query: %s", err) + } + if !json.Valid(queryData) { + return errors.New("query data must be json") + } + + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.SmartContractState( + context.Background(), + &types.QuerySmartContractStateRequest{ + Address: args[0], + QueryData: queryData, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + decoder.RegisterFlags(cmd.PersistentFlags(), "query argument") + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +// GetCmdGetContractHistory prints the code history for a given contract +func GetCmdGetContractHistory() *cobra.Command { + cmd := &cobra.Command{ + Use: "contract-history [bech32_address]", + Short: "Prints out the code history for a contract given its address", + Long: "Prints out the code history for a contract given its address", + Aliases: []string{"history", "hist", "ch"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + _, err = sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags())) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.ContractHistory( + context.Background(), + &types.QueryContractHistoryRequest{ + Address: args[0], + Pagination: pageReq, + }, + ) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "contract history") + return cmd +} + +// GetCmdListPinnedCode lists all wasm code ids that are pinned +func GetCmdListPinnedCode() *cobra.Command { + cmd := &cobra.Command{ + Use: "pinned", + Short: "List all pinned code ids", + Long: "List all pinned code ids", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags())) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.PinnedCodes( + context.Background(), + &types.QueryPinnedCodesRequest{ + Pagination: pageReq, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "list codes") + return cmd +} + +// GetCmdListContractsByCreator lists all contracts by creator +func GetCmdListContractsByCreator() *cobra.Command { + cmd := &cobra.Command{ + Use: "list-contracts-by-creator [creator]", + Short: "List all contracts by creator", + Long: "List all contracts by creator", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + _, err = sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + pageReq, err := client.ReadPageRequest(withPageKeyDecoded(cmd.Flags())) + if err != nil { + return err + } + + queryClient := types.NewQueryClient(clientCtx) + res, err := queryClient.ContractsByCreator( + context.Background(), + &types.QueryContractsByCreatorRequest{ + CreatorAddress: args[0], + Pagination: pageReq, + }, + ) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + SilenceUsage: true, + } + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +type argumentDecoder struct { + // dec is the default decoder + dec func(string) ([]byte, error) + asciiF, hexF, b64F bool +} + +func newArgDecoder(def func(string) ([]byte, error)) *argumentDecoder { + return &argumentDecoder{dec: def} +} + +func (a *argumentDecoder) RegisterFlags(f *flag.FlagSet, argName string) { + f.BoolVar(&a.asciiF, "ascii", false, "ascii encoded "+argName) + f.BoolVar(&a.hexF, "hex", false, "hex encoded "+argName) + f.BoolVar(&a.b64F, "b64", false, "base64 encoded "+argName) +} + +func (a *argumentDecoder) DecodeString(s string) ([]byte, error) { + found := -1 + for i, v := range []*bool{&a.asciiF, &a.hexF, &a.b64F} { + if !*v { + continue + } + if found != -1 { + return nil, errors.New("multiple decoding flags used") + } + found = i + } + switch found { + case 0: + return asciiDecodeString(s) + case 1: + return hex.DecodeString(s) + case 2: + return base64.StdEncoding.DecodeString(s) + default: + return a.dec(s) + } +} + +func asciiDecodeString(s string) ([]byte, error) { + return []byte(s), nil +} + +// sdk ReadPageRequest expects binary but we encoded to base64 in our marshaller +func withPageKeyDecoded(flagSet *flag.FlagSet) *flag.FlagSet { + encoded, err := flagSet.GetString(flags.FlagPageKey) + if err != nil { + panic(err.Error()) + } + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + panic(err.Error()) + } + err = flagSet.Set(flags.FlagPageKey, string(raw)) + if err != nil { + panic(err.Error()) + } + return flagSet +} + +// GetCmdQueryParams implements a command to return the current wasm +// parameters. +func GetCmdQueryParams() *cobra.Command { + cmd := &cobra.Command{ + Use: "params", + Short: "Query the current wasm parameters", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + params := &types.QueryParamsRequest{} + res, err := queryClient.Params(cmd.Context(), params) + if err != nil { + return err + } + + return clientCtx.PrintProto(&res.Params) + }, + SilenceUsage: true, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/wasm/client/cli/tx.go b/x/wasm/client/cli/tx.go new file mode 100644 index 00000000..092d4288 --- /dev/null +++ b/x/wasm/client/cli/tx.go @@ -0,0 +1,544 @@ +package cli + +import ( + "encoding/hex" + "errors" + "fmt" + "os" + "strconv" + "time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/cerc-io/laconicd/x/wasm/ioutils" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +const ( + flagAmount = "amount" + flagLabel = "label" + flagSource = "code-source-url" + flagBuilder = "builder" + flagCodeHash = "code-hash" + flagAdmin = "admin" + flagNoAdmin = "no-admin" + flagFixMsg = "fix-msg" + flagRunAs = "run-as" + flagInstantiateByEverybody = "instantiate-everybody" + flagInstantiateNobody = "instantiate-nobody" + flagInstantiateByAddress = "instantiate-only-address" + flagInstantiateByAnyOfAddress = "instantiate-anyof-addresses" + flagUnpinCode = "unpin-code" + flagAllowedMsgKeys = "allow-msg-keys" + flagAllowedRawMsgs = "allow-raw-msgs" + flagExpiration = "expiration" + flagMaxCalls = "max-calls" + flagMaxFunds = "max-funds" + flagAllowAllMsgs = "allow-all-messages" + flagNoTokenTransfer = "no-token-transfer" //nolint:gosec +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Wasm transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + SilenceUsage: true, + } + txCmd.AddCommand( + StoreCodeCmd(), + InstantiateContractCmd(), + InstantiateContract2Cmd(), + ExecuteContractCmd(), + MigrateContractCmd(), + UpdateContractAdminCmd(), + ClearContractAdminCmd(), + GrantAuthorizationCmd(), + UpdateInstantiateConfigCmd(), + ) + return txCmd +} + +// StoreCodeCmd will upload code to be reused. +func StoreCodeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "store [wasm file]", + Short: "Upload a wasm binary", + Aliases: []string{"upload", "st", "s"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + msg, err := parseStoreCodeArgs(args[0], clientCtx.GetFromAddress(), cmd.Flags()) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + SilenceUsage: true, + } + + addInstantiatePermissionFlags(cmd) + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func parseStoreCodeArgs(file string, sender sdk.AccAddress, flags *flag.FlagSet) (types.MsgStoreCode, error) { + wasm, err := os.ReadFile(file) + if err != nil { + return types.MsgStoreCode{}, err + } + + // gzip the wasm file + if ioutils.IsWasm(wasm) { + wasm, err = ioutils.GzipIt(wasm) + + if err != nil { + return types.MsgStoreCode{}, err + } + } else if !ioutils.IsGzip(wasm) { + return types.MsgStoreCode{}, fmt.Errorf("invalid input file. Use wasm binary or gzip") + } + + perm, err := parseAccessConfigFlags(flags) + if err != nil { + return types.MsgStoreCode{}, err + } + + msg := types.MsgStoreCode{ + Sender: sender.String(), + WASMByteCode: wasm, + InstantiatePermission: perm, + } + return msg, nil +} + +func parseAccessConfigFlags(flags *flag.FlagSet) (*types.AccessConfig, error) { + addrs, err := flags.GetStringSlice(flagInstantiateByAnyOfAddress) + if err != nil { + return nil, fmt.Errorf("flag any of: %s", err) + } + if len(addrs) != 0 { + acceptedAddrs := make([]sdk.AccAddress, len(addrs)) + for i, v := range addrs { + acceptedAddrs[i], err = sdk.AccAddressFromBech32(v) + if err != nil { + return nil, fmt.Errorf("parse %q: %w", v, err) + } + } + x := types.AccessTypeAnyOfAddresses.With(acceptedAddrs...) + return &x, nil + } + + onlyAddrStr, err := flags.GetString(flagInstantiateByAddress) + if err != nil { + return nil, fmt.Errorf("instantiate by address: %s", err) + } + if onlyAddrStr != "" { + return nil, fmt.Errorf("not supported anymore. Use: %s", flagInstantiateByAnyOfAddress) + } + everybodyStr, err := flags.GetString(flagInstantiateByEverybody) + if err != nil { + return nil, fmt.Errorf("instantiate by everybody: %s", err) + } + if everybodyStr != "" { + ok, err := strconv.ParseBool(everybodyStr) + if err != nil { + return nil, fmt.Errorf("boolean value expected for instantiate by everybody: %s", err) + } + if ok { + return &types.AllowEverybody, nil + } + } + + nobodyStr, err := flags.GetString(flagInstantiateNobody) + if err != nil { + return nil, fmt.Errorf("instantiate by nobody: %s", err) + } + if nobodyStr != "" { + ok, err := strconv.ParseBool(nobodyStr) + if err != nil { + return nil, fmt.Errorf("boolean value expected for instantiate by nobody: %s", err) + } + if ok { + return &types.AllowNobody, nil + } + } + return nil, nil +} + +func addInstantiatePermissionFlags(cmd *cobra.Command) { + cmd.Flags().String(flagInstantiateByEverybody, "", "Everybody can instantiate a contract from the code, optional") + cmd.Flags().String(flagInstantiateNobody, "", "Nobody except the governance process can instantiate a contract from the code, optional") + cmd.Flags().String(flagInstantiateByAddress, "", fmt.Sprintf("Removed: use %s instead", flagInstantiateByAnyOfAddress)) + cmd.Flags().StringSlice(flagInstantiateByAnyOfAddress, []string{}, "Any of the addresses can instantiate a contract from the code, optional") +} + +// InstantiateContractCmd will instantiate a contract from previously uploaded code. +func InstantiateContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "instantiate [code_id_int64] [json_encoded_init_args] --label [text] --admin [address,optional] --amount [coins,optional] ", + Short: "Instantiate a wasm contract", + Long: fmt.Sprintf(`Creates a new instance of an uploaded wasm code with the given 'constructor' message. +Each contract instance has a unique address assigned. +Example: +$ %s tx wasm instantiate 1 '{"foo":"bar"}' --admin="$(%s keys show mykey -a)" \ + --from mykey --amount="100ustake" --label "local0.1.0" +`, version.AppName, version.AppName), + Aliases: []string{"start", "init", "inst", "i"}, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + msg, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.GetFromAddress(), cmd.Flags()) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") + cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists") + cmd.Flags().String(flagAdmin, "", "Address or key name of an admin") + cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin") + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +// InstantiateContract2Cmd will instantiate a contract from previously uploaded code with predicable address generated +func InstantiateContract2Cmd() *cobra.Command { + decoder := newArgDecoder(hex.DecodeString) + cmd := &cobra.Command{ + Use: "instantiate2 [code_id_int64] [json_encoded_init_args] [salt] --label [text] --admin [address,optional] --amount [coins,optional] " + + "--fix-msg [bool,optional]", + Short: "Instantiate a wasm contract with predictable address", + Long: fmt.Sprintf(`Creates a new instance of an uploaded wasm code with the given 'constructor' message. +Each contract instance has a unique address assigned. They are assigned automatically but in order to have predictable addresses +for special use cases, the given 'salt' argument and '--fix-msg' parameters can be used to generate a custom address. + +Predictable address example (also see '%s query wasm build-address -h'): +$ %s tx wasm instantiate2 1 '{"foo":"bar"}' $(echo -n "testing" | xxd -ps) --admin="$(%s keys show mykey -a)" \ + --from mykey --amount="100ustake" --label "local0.1.0" \ + --fix-msg +`, version.AppName, version.AppName, version.AppName), + Aliases: []string{"start", "init", "inst", "i"}, + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + salt, err := decoder.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("salt: %w", err) + } + fixMsg, err := cmd.Flags().GetBool(flagFixMsg) + if err != nil { + return fmt.Errorf("fix msg: %w", err) + } + data, err := parseInstantiateArgs(args[0], args[1], clientCtx.Keyring, clientCtx.GetFromAddress(), cmd.Flags()) + if err != nil { + return err + } + msg := &types.MsgInstantiateContract2{ + Sender: data.Sender, + Admin: data.Admin, + CodeID: data.CodeID, + Label: data.Label, + Msg: data.Msg, + Funds: data.Funds, + Salt: salt, + FixMsg: fixMsg, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + SilenceUsage: true, + } + + cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") + cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists") + cmd.Flags().String(flagAdmin, "", "Address or key name of an admin") + cmd.Flags().Bool(flagNoAdmin, false, "You must set this explicitly if you don't want an admin") + cmd.Flags().Bool(flagFixMsg, false, "An optional flag to include the json_encoded_init_args for the predictable address generation mode") + decoder.RegisterFlags(cmd.PersistentFlags(), "salt") + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func parseInstantiateArgs(rawCodeID, initMsg string, kr keyring.Keyring, sender sdk.AccAddress, flags *flag.FlagSet) (*types.MsgInstantiateContract, error) { + // get the id of the code to instantiate + codeID, err := strconv.ParseUint(rawCodeID, 10, 64) + if err != nil { + return nil, err + } + + amountStr, err := flags.GetString(flagAmount) + if err != nil { + return nil, fmt.Errorf("amount: %s", err) + } + amount, err := sdk.ParseCoinsNormalized(amountStr) + if err != nil { + return nil, fmt.Errorf("amount: %s", err) + } + label, err := flags.GetString(flagLabel) + if err != nil { + return nil, fmt.Errorf("label: %s", err) + } + if label == "" { + return nil, errors.New("label is required on all contracts") + } + adminStr, err := flags.GetString(flagAdmin) + if err != nil { + return nil, fmt.Errorf("admin: %s", err) + } + + noAdmin, err := flags.GetBool(flagNoAdmin) + if err != nil { + return nil, fmt.Errorf("no-admin: %s", err) + } + + // ensure sensible admin is set (or explicitly immutable) + if adminStr == "" && !noAdmin { + return nil, fmt.Errorf("you must set an admin or explicitly pass --no-admin to make it immutible (wasmd issue #719)") + } + if adminStr != "" && noAdmin { + return nil, fmt.Errorf("you set an admin and passed --no-admin, those cannot both be true") + } + + if adminStr != "" { + addr, err := sdk.AccAddressFromBech32(adminStr) + if err != nil { + info, err := kr.Key(adminStr) + if err != nil { + return nil, fmt.Errorf("admin %s", err) + } + adminStr = info.GetAddress().String() + } else { + adminStr = addr.String() + } + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgInstantiateContract{ + Sender: sender.String(), + CodeID: codeID, + Label: label, + Funds: amount, + Msg: []byte(initMsg), + Admin: adminStr, + } + return &msg, nil +} + +// ExecuteContractCmd will instantiate a contract from previously uploaded code. +func ExecuteContractCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "execute [contract_addr_bech32] [json_encoded_send_args] --amount [coins,optional]", + Short: "Execute a command on a wasm contract", + Aliases: []string{"run", "call", "exec", "ex", "e"}, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg, err := parseExecuteArgs(args[0], args[1], clientCtx.GetFromAddress(), cmd.Flags()) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + SilenceUsage: true, + } + + cmd.Flags().String(flagAmount, "", "Coins to send to the contract along with command") + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func parseExecuteArgs(contractAddr string, execMsg string, sender sdk.AccAddress, flags *flag.FlagSet) (types.MsgExecuteContract, error) { + amountStr, err := flags.GetString(flagAmount) + if err != nil { + return types.MsgExecuteContract{}, fmt.Errorf("amount: %s", err) + } + + amount, err := sdk.ParseCoinsNormalized(amountStr) + if err != nil { + return types.MsgExecuteContract{}, err + } + + return types.MsgExecuteContract{ + Sender: sender.String(), + Contract: contractAddr, + Funds: amount, + Msg: []byte(execMsg), + }, nil +} + +func GrantAuthorizationCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "grant [grantee] [message_type=\"execution\"|\"migration\"] [contract_addr_bech32] --allow-raw-msgs [msg1,msg2,...] --allow-msg-keys [key1,key2,...] --allow-all-messages", + Short: "Grant authorization to an address", + Long: fmt.Sprintf(`Grant authorization to an address. +Examples: +$ %s tx grant execution --allow-all-messages --max-calls 1 --no-token-transfer --expiration 1667979596 + +$ %s tx grant execution --allow-all-messages --max-funds 100000uwasm --expiration 1667979596 + +$ %s tx grant execution --allow-all-messages --max-calls 5 --max-funds 100000uwasm --expiration 1667979596 +`, version.AppName, version.AppName, version.AppName), + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + grantee, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + contract, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msgKeys, err := cmd.Flags().GetStringSlice(flagAllowedMsgKeys) + if err != nil { + return err + } + + rawMsgs, err := cmd.Flags().GetStringSlice(flagAllowedRawMsgs) + if err != nil { + return err + } + + maxFundsStr, err := cmd.Flags().GetString(flagMaxFunds) + if err != nil { + return fmt.Errorf("max funds: %s", err) + } + + maxCalls, err := cmd.Flags().GetUint64(flagMaxCalls) + if err != nil { + return err + } + + exp, err := cmd.Flags().GetInt64(flagExpiration) + if err != nil { + return err + } + if exp == 0 { + return errors.New("expiration must be set") + } + + allowAllMsgs, err := cmd.Flags().GetBool(flagAllowAllMsgs) + if err != nil { + return err + } + + noTokenTransfer, err := cmd.Flags().GetBool(flagNoTokenTransfer) + if err != nil { + return err + } + + var limit types.ContractAuthzLimitX + switch { + case maxFundsStr != "" && maxCalls != 0 && !noTokenTransfer: + maxFunds, err := sdk.ParseCoinsNormalized(maxFundsStr) + if err != nil { + return fmt.Errorf("max funds: %s", err) + } + limit = types.NewCombinedLimit(maxCalls, maxFunds...) + case maxFundsStr != "" && maxCalls == 0 && !noTokenTransfer: + maxFunds, err := sdk.ParseCoinsNormalized(maxFundsStr) + if err != nil { + return fmt.Errorf("max funds: %s", err) + } + limit = types.NewMaxFundsLimit(maxFunds...) + case maxCalls != 0 && noTokenTransfer && maxFundsStr == "": + limit = types.NewMaxCallsLimit(maxCalls) + default: + return errors.New("invalid limit setup") + } + + var filter types.ContractAuthzFilterX + switch { + case allowAllMsgs && len(msgKeys) != 0 || allowAllMsgs && len(rawMsgs) != 0 || len(msgKeys) != 0 && len(rawMsgs) != 0: + return errors.New("cannot set more than one filter within one grant") + case allowAllMsgs: + filter = types.NewAllowAllMessagesFilter() + case len(msgKeys) != 0: + filter = types.NewAcceptedMessageKeysFilter(msgKeys...) + case len(rawMsgs) != 0: + msgs := make([]types.RawContractMessage, len(rawMsgs)) + for i, msg := range rawMsgs { + msgs[i] = types.RawContractMessage(msg) + } + filter = types.NewAcceptedMessagesFilter(msgs...) + default: + return errors.New("invalid filter setup") + } + + grant, err := types.NewContractGrant(contract, limit, filter) + if err != nil { + return err + } + + var authorization authz.Authorization + switch args[1] { + case "execution": + authorization = types.NewContractExecutionAuthorization(*grant) + case "migration": + authorization = types.NewContractMigrationAuthorization(*grant) + default: + return fmt.Errorf("%s authorization type not supported", args[1]) + } + + grantMsg, err := authz.NewMsgGrant(clientCtx.GetFromAddress(), grantee, authorization, time.Unix(0, exp)) + if err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), grantMsg) + }, + } + flags.AddTxFlagsToCmd(cmd) + cmd.Flags().StringSlice(flagAllowedMsgKeys, []string{}, "Allowed msg keys") + cmd.Flags().StringSlice(flagAllowedRawMsgs, []string{}, "Allowed raw msgs") + cmd.Flags().Uint64(flagMaxCalls, 0, "Maximal number of calls to the contract") + cmd.Flags().String(flagMaxFunds, "", "Maximal amount of tokens transferable to the contract.") + cmd.Flags().Int64(flagExpiration, 0, "The Unix timestamp.") + cmd.Flags().Bool(flagAllowAllMsgs, false, "Allow all messages") + cmd.Flags().Bool(flagNoTokenTransfer, false, "Don't allow token transfer") + return cmd +} diff --git a/x/wasm/client/cli/tx_test.go b/x/wasm/client/cli/tx_test.go new file mode 100644 index 00000000..15bbc15a --- /dev/null +++ b/x/wasm/client/cli/tx_test.go @@ -0,0 +1,59 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestParseAccessConfigFlags(t *testing.T) { + specs := map[string]struct { + args []string + expCfg *types.AccessConfig + expErr bool + }{ + "nobody": { + args: []string{"--instantiate-nobody=true"}, + expCfg: &types.AccessConfig{Permission: types.AccessTypeNobody}, + }, + "everybody": { + args: []string{"--instantiate-everybody=true"}, + expCfg: &types.AccessConfig{Permission: types.AccessTypeEverybody}, + }, + "only address": { + args: []string{"--instantiate-only-address=cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x"}, + expErr: true, + }, + "only address - invalid": { + args: []string{"--instantiate-only-address=foo"}, + expErr: true, + }, + "any of address": { + args: []string{"--instantiate-anyof-addresses=cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"}, + expCfg: &types.AccessConfig{Permission: types.AccessTypeAnyOfAddresses, Addresses: []string{"cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x", "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr"}}, + }, + "any of address - invalid": { + args: []string{"--instantiate-anyof-addresses=cosmos1vx8knpllrj7n963p9ttd80w47kpacrhuts497x,foo"}, + expErr: true, + }, + "not set": { + args: []string{}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + flags := StoreCodeCmd().Flags() + require.NoError(t, flags.Parse(spec.args)) + gotCfg, gotErr := parseAccessConfigFlags(flags) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expCfg, gotCfg) + }) + } +} diff --git a/x/wasm/client/proposal_handler.go b/x/wasm/client/proposal_handler.go new file mode 100644 index 00000000..cf7b3d4e --- /dev/null +++ b/x/wasm/client/proposal_handler.go @@ -0,0 +1,25 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + + "github.com/cerc-io/laconicd/x/wasm/client/cli" + "github.com/cerc-io/laconicd/x/wasm/client/rest" //nolint:staticcheck +) + +// ProposalHandlers define the wasm cli proposal types and rest handler. +// Deprecated: the rest package will be removed. You can use the GRPC gateway instead +var ProposalHandlers = []govclient.ProposalHandler{ + govclient.NewProposalHandler(cli.ProposalStoreCodeCmd, rest.StoreCodeProposalHandler), + govclient.NewProposalHandler(cli.ProposalInstantiateContractCmd, rest.InstantiateProposalHandler), + govclient.NewProposalHandler(cli.ProposalMigrateContractCmd, rest.MigrateProposalHandler), + govclient.NewProposalHandler(cli.ProposalExecuteContractCmd, rest.ExecuteProposalHandler), + govclient.NewProposalHandler(cli.ProposalSudoContractCmd, rest.SudoProposalHandler), + govclient.NewProposalHandler(cli.ProposalUpdateContractAdminCmd, rest.UpdateContractAdminProposalHandler), + govclient.NewProposalHandler(cli.ProposalClearContractAdminCmd, rest.ClearContractAdminProposalHandler), + govclient.NewProposalHandler(cli.ProposalPinCodesCmd, rest.PinCodeProposalHandler), + govclient.NewProposalHandler(cli.ProposalUnpinCodesCmd, rest.UnpinCodeProposalHandler), + govclient.NewProposalHandler(cli.ProposalUpdateInstantiateConfigCmd, rest.UpdateInstantiateConfigProposalHandler), + govclient.NewProposalHandler(cli.ProposalStoreAndInstantiateContractCmd, rest.EmptyRestHandler), + govclient.NewProposalHandler(cli.ProposalInstantiateContract2Cmd, rest.EmptyRestHandler), +} diff --git a/x/wasm/client/proposal_handler_test.go b/x/wasm/client/proposal_handler_test.go new file mode 100644 index 00000000..dd4099da --- /dev/null +++ b/x/wasm/client/proposal_handler_test.go @@ -0,0 +1,381 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper" +) + +func TestGovRestHandlers(t *testing.T) { + type dict map[string]interface{} + var ( + anyAddress = "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz" + aBaseReq = dict{ + "from": anyAddress, + "memo": "rest test", + "chain_id": "testing", + "account_number": "1", + "sequence": "1", + "fees": []dict{{"denom": "ustake", "amount": "1000000"}}, + } + ) + encodingConfig := keeper.MakeEncodingConfig(t) + clientCtx := client.Context{}. + WithCodec(encodingConfig.Marshaler). + WithTxConfig(encodingConfig.TxConfig). + WithLegacyAmino(encodingConfig.Amino). + WithInput(os.Stdin). + WithAccountRetriever(authtypes.AccountRetriever{}). + WithBroadcastMode(flags.BroadcastBlock). + WithChainID("testing") + + // router setup as in gov/client/rest/tx.go + propSubRtr := mux.NewRouter().PathPrefix("/gov/proposals").Subrouter() + for _, ph := range ProposalHandlers { + r := ph.RESTHandler(clientCtx) + propSubRtr.HandleFunc(fmt.Sprintf("/%s", r.SubRoute), r.Handler).Methods("POST") + } + + specs := map[string]struct { + srcBody dict + srcPath string + expCode int + }{ + "store-code": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": []byte("valid wasm byte code"), + "source": "https://example.com/", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "store-code without verification info": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": []byte("valid wasm byte code"), + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "store-code without permission": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": []byte("valid wasm byte code"), + "source": "https://example.com/", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "store-code invalid permission": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": []byte("valid wasm byte code"), + "source": "https://example.com/", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "instantiate_permission": dict{ + "permission": "Nobody", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "store-code with incomplete proposal data: blank title": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": []byte("valid wasm byte code"), + "source": "https://example.com/", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "store-code with incomplete content data: no wasm_byte_code": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": "", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "source": "https://example.com/", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "store-code with incomplete content data: no builder": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": "", + "source": "https://example.com/", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "store-code with incomplete content data: no code hash": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": "", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "source": "https://example.com/", + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "store-code with incomplete content data: no source": { + srcPath: "/gov/proposals/wasm_store_code", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "store-code", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "wasm_byte_code": "", + "builder": "cosmwasm/workspace-optimizer:v0.12.9", + "code_hash": "79F174F09BFE3F83398BF7C147929D5F735161BD46D645E85216BB13BF91D42D", + "instantiate_permission": dict{ + "permission": "OnlyAddress", + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + }, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "instantiate contract": { + srcPath: "/gov/proposals/wasm_instantiate", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "instantiate", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "admin": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "code_id": "1", + "label": "https://example.com/", + "msg": dict{"recipient": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz"}, + "funds": []dict{{"denom": "ustake", "amount": "100"}}, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "migrate contract": { + srcPath: "/gov/proposals/wasm_migrate", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "code_id": "1", + "msg": dict{"foo": "bar"}, + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "execute contract": { + srcPath: "/gov/proposals/wasm_execute", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "msg": dict{"foo": "bar"}, + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "execute contract fails with no run_as": { + srcPath: "/gov/proposals/wasm_execute", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "msg": dict{"foo": "bar"}, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "execute contract fails with no message": { + srcPath: "/gov/proposals/wasm_execute", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "sudo contract": { + srcPath: "/gov/proposals/wasm_sudo", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "msg": dict{"foo": "bar"}, + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "sudo contract fails with no message": { + srcPath: "/gov/proposals/wasm_sudo", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusBadRequest, + }, + "update contract admin": { + srcPath: "/gov/proposals/wasm_update_admin", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "new_admin": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "clear contract admin": { + srcPath: "/gov/proposals/wasm_clear_admin", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + src, err := json.Marshal(spec.srcBody) + require.NoError(t, err) + + // when + r := httptest.NewRequest("POST", spec.srcPath, bytes.NewReader(src)) + w := httptest.NewRecorder() + propSubRtr.ServeHTTP(w, r) + + // then + require.Equal(t, spec.expCode, w.Code, w.Body.String()) + }) + } +} diff --git a/x/wasm/client/rest/gov.go b/x/wasm/client/rest/gov.go new file mode 100644 index 00000000..b048cf87 --- /dev/null +++ b/x/wasm/client/rest/gov.go @@ -0,0 +1,547 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +type StoreCodeProposalJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + RunAs string `json:"run_as" yaml:"run_as"` + // WASMByteCode can be raw or gzip compressed + WASMByteCode []byte `json:"wasm_byte_code" yaml:"wasm_byte_code"` + // InstantiatePermission to apply on contract creation, optional + InstantiatePermission *types.AccessConfig `json:"instantiate_permission" yaml:"instantiate_permission"` + + // UnpinCode indicates if the code should not be pinned as part of the proposal. + UnpinCode bool `json:"unpin_code" yaml:"unpin_code"` + + // Source is the URL where the code is hosted + Source string `json:"source" yaml:"source"` + // Builder is the docker image used to build the code deterministically, used for smart + // contract verification + Builder string `json:"builder" yaml:"builder"` + // CodeHash is the SHA256 sum of the code outputted by optimizer, used for smart contract verification + CodeHash []byte `json:"code_hash" yaml:"code_hash"` +} + +func (s StoreCodeProposalJSONReq) Content() govtypes.Content { + return &types.StoreCodeProposal{ + Title: s.Title, + Description: s.Description, + RunAs: s.RunAs, + WASMByteCode: s.WASMByteCode, + InstantiatePermission: s.InstantiatePermission, + UnpinCode: s.UnpinCode, + Source: s.Source, + Builder: s.Builder, + CodeHash: s.CodeHash, + } +} + +func (s StoreCodeProposalJSONReq) GetProposer() string { + return s.Proposer +} + +func (s StoreCodeProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s StoreCodeProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func StoreCodeProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_store_code", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req StoreCodeProposalJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type InstantiateProposalJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + RunAs string `json:"run_as" yaml:"run_as"` + // Admin is an optional address that can execute migrations + Admin string `json:"admin,omitempty" yaml:"admin"` + Code uint64 `json:"code_id" yaml:"code_id"` + Label string `json:"label" yaml:"label"` + Msg json.RawMessage `json:"msg" yaml:"msg"` + Funds sdk.Coins `json:"funds" yaml:"funds"` +} + +func (s InstantiateProposalJSONReq) Content() govtypes.Content { + return &types.InstantiateContractProposal{ + Title: s.Title, + Description: s.Description, + RunAs: s.RunAs, + Admin: s.Admin, + CodeID: s.Code, + Label: s.Label, + Msg: types.RawContractMessage(s.Msg), + Funds: s.Funds, + } +} + +func (s InstantiateProposalJSONReq) GetProposer() string { + return s.Proposer +} + +func (s InstantiateProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s InstantiateProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func InstantiateProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_instantiate", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req InstantiateProposalJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type MigrateProposalJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + Contract string `json:"contract" yaml:"contract"` + Code uint64 `json:"code_id" yaml:"code_id"` + Msg json.RawMessage `json:"msg" yaml:"msg"` +} + +func (s MigrateProposalJSONReq) Content() govtypes.Content { + return &types.MigrateContractProposal{ + Title: s.Title, + Description: s.Description, + Contract: s.Contract, + CodeID: s.Code, + Msg: types.RawContractMessage(s.Msg), + } +} + +func (s MigrateProposalJSONReq) GetProposer() string { + return s.Proposer +} + +func (s MigrateProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s MigrateProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func MigrateProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_migrate", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req MigrateProposalJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type ExecuteProposalJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + Contract string `json:"contract" yaml:"contract"` + Msg json.RawMessage `json:"msg" yaml:"msg"` + // RunAs is the role that is passed to the contract's environment + RunAs string `json:"run_as" yaml:"run_as"` + Funds sdk.Coins `json:"funds" yaml:"funds"` +} + +func (s ExecuteProposalJSONReq) Content() govtypes.Content { + return &types.ExecuteContractProposal{ + Title: s.Title, + Description: s.Description, + Contract: s.Contract, + Msg: types.RawContractMessage(s.Msg), + RunAs: s.RunAs, + Funds: s.Funds, + } +} + +func (s ExecuteProposalJSONReq) GetProposer() string { + return s.Proposer +} + +func (s ExecuteProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s ExecuteProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func ExecuteProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_execute", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req ExecuteProposalJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type SudoProposalJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + Contract string `json:"contract" yaml:"contract"` + Msg json.RawMessage `json:"msg" yaml:"msg"` +} + +func (s SudoProposalJSONReq) Content() govtypes.Content { + return &types.SudoContractProposal{ + Title: s.Title, + Description: s.Description, + Contract: s.Contract, + Msg: types.RawContractMessage(s.Msg), + } +} + +func (s SudoProposalJSONReq) GetProposer() string { + return s.Proposer +} + +func (s SudoProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s SudoProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func SudoProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_sudo", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req SudoProposalJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type UpdateAdminJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + NewAdmin string `json:"new_admin" yaml:"new_admin"` + Contract string `json:"contract" yaml:"contract"` +} + +func (s UpdateAdminJSONReq) Content() govtypes.Content { + return &types.UpdateAdminProposal{ + Title: s.Title, + Description: s.Description, + Contract: s.Contract, + NewAdmin: s.NewAdmin, + } +} + +func (s UpdateAdminJSONReq) GetProposer() string { + return s.Proposer +} + +func (s UpdateAdminJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s UpdateAdminJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func UpdateContractAdminProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_update_admin", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req UpdateAdminJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type ClearAdminJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + Contract string `json:"contract" yaml:"contract"` +} + +func (s ClearAdminJSONReq) Content() govtypes.Content { + return &types.ClearAdminProposal{ + Title: s.Title, + Description: s.Description, + Contract: s.Contract, + } +} + +func (s ClearAdminJSONReq) GetProposer() string { + return s.Proposer +} + +func (s ClearAdminJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s ClearAdminJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func ClearContractAdminProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "wasm_clear_admin", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req ClearAdminJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type PinCodeJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + CodeIDs []uint64 `json:"code_ids" yaml:"code_ids"` +} + +func (s PinCodeJSONReq) Content() govtypes.Content { + return &types.PinCodesProposal{ + Title: s.Title, + Description: s.Description, + CodeIDs: s.CodeIDs, + } +} + +func (s PinCodeJSONReq) GetProposer() string { + return s.Proposer +} + +func (s PinCodeJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s PinCodeJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func PinCodeProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "pin_code", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req PinCodeJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type UnpinCodeJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + CodeIDs []uint64 `json:"code_ids" yaml:"code_ids"` +} + +func (s UnpinCodeJSONReq) Content() govtypes.Content { + return &types.UnpinCodesProposal{ + Title: s.Title, + Description: s.Description, + CodeIDs: s.CodeIDs, + } +} + +func (s UnpinCodeJSONReq) GetProposer() string { + return s.Proposer +} + +func (s UnpinCodeJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s UnpinCodeJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func UnpinCodeProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "unpin_code", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req UnpinCodeJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type UpdateInstantiateConfigProposalJSONReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Proposer string `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + AccessConfigUpdates []types.AccessConfigUpdate `json:"access_config_updates" yaml:"access_config_updates"` +} + +func (s UpdateInstantiateConfigProposalJSONReq) Content() govtypes.Content { + return &types.UpdateInstantiateConfigProposal{ + Title: s.Title, + Description: s.Description, + AccessConfigUpdates: s.AccessConfigUpdates, + } +} + +func (s UpdateInstantiateConfigProposalJSONReq) GetProposer() string { + return s.Proposer +} + +func (s UpdateInstantiateConfigProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} + +func (s UpdateInstantiateConfigProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func UpdateInstantiateConfigProposalHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "update_instantiate_config", + Handler: func(w http.ResponseWriter, r *http.Request) { + var req UpdateInstantiateConfigProposalJSONReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type wasmProposalData interface { + Content() govtypes.Content + GetProposer() string + GetDeposit() sdk.Coins + GetBaseReq() rest.BaseReq +} + +func toStdTxResponse(cliCtx client.Context, w http.ResponseWriter, data wasmProposalData) { + proposerAddr, err := sdk.AccAddressFromBech32(data.GetProposer()) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + msg, err := govtypes.NewMsgSubmitProposal(data.Content(), data.GetDeposit(), proposerAddr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + baseReq := data.GetBaseReq().Sanitize() + if !baseReq.ValidateBasic(w) { + return + } + tx.WriteGeneratedTxResponse(cliCtx, w, baseReq, msg) +} + +func EmptyRestHandler(cliCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "unsupported", + Handler: func(w http.ResponseWriter, r *http.Request) { + rest.WriteErrorResponse(w, http.StatusBadRequest, "Legacy REST Routes are not supported for gov proposals") + }, + } +} diff --git a/x/wasm/client/rest/new_tx.go b/x/wasm/client/rest/new_tx.go new file mode 100644 index 00000000..89ebaf48 --- /dev/null +++ b/x/wasm/client/rest/new_tx.go @@ -0,0 +1,86 @@ +package rest + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/gorilla/mux" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func registerNewTxRoutes(cliCtx client.Context, r *mux.Router) { + r.HandleFunc("/wasm/contract/{contractAddr}/admin", setContractAdminHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/wasm/contract/{contractAddr}/code", migrateContractHandlerFn(cliCtx)).Methods("PUT") +} + +type migrateContractReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Admin string `json:"admin,omitempty" yaml:"admin"` + CodeID uint64 `json:"code_id" yaml:"code_id"` + Msg []byte `json:"msg,omitempty" yaml:"msg"` +} + +type updateContractAdministrateReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Admin string `json:"admin,omitempty" yaml:"admin"` +} + +func setContractAdminHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req updateContractAdministrateReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + msg := &types.MsgUpdateAdmin{ + Sender: req.BaseReq.From, + NewAdmin: req.Admin, + Contract: contractAddr, + } + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, msg) + } +} + +func migrateContractHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req migrateContractReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + msg := &types.MsgMigrateContract{ + Sender: req.BaseReq.From, + Contract: contractAddr, + CodeID: req.CodeID, + Msg: req.Msg, + } + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, msg) + } +} diff --git a/x/wasm/client/rest/query.go b/x/wasm/client/rest/query.go new file mode 100644 index 00000000..42c074e3 --- /dev/null +++ b/x/wasm/client/rest/query.go @@ -0,0 +1,270 @@ +package rest + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/gorilla/mux" + + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func registerQueryRoutes(cliCtx client.Context, r *mux.Router) { + r.HandleFunc("/wasm/code", listCodesHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/wasm/code/{codeID}", queryCodeHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/wasm/code/{codeID}/contracts", listContractsByCodeHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/wasm/contract/{contractAddr}", queryContractHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/wasm/contract/{contractAddr}/state", queryContractStateAllHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/wasm/contract/{contractAddr}/history", queryContractHistoryFn(cliCtx)).Methods("GET") + r.HandleFunc("/wasm/contract/{contractAddr}/smart/{query}", queryContractStateSmartHandlerFn(cliCtx)).Queries("encoding", "{encoding}").Methods("GET") + r.HandleFunc("/wasm/contract/{contractAddr}/raw/{key}", queryContractStateRawHandlerFn(cliCtx)).Queries("encoding", "{encoding}").Methods("GET") +} + +func listCodesHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, keeper.QueryListCode) + res, height, err := cliCtx.Query(route) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, json.RawMessage(res)) + } +} + +func queryCodeHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + codeID, err := strconv.ParseUint(mux.Vars(r)["codeID"], 10, 64) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%d", types.QuerierRoute, keeper.QueryGetCode, codeID) + res, height, err := cliCtx.Query(route) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + if len(res) == 0 { + rest.WriteErrorResponse(w, http.StatusNotFound, "contract not found") + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, json.RawMessage(res)) + } +} + +func listContractsByCodeHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + codeID, err := strconv.ParseUint(mux.Vars(r)["codeID"], 10, 64) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%d", types.QuerierRoute, keeper.QueryListContractByCode, codeID) + res, height, err := cliCtx.Query(route) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, json.RawMessage(res)) + } +} + +func queryContractHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContract, addr.String()) + res, height, err := cliCtx.Query(route) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, json.RawMessage(res)) + } +} + +func queryContractStateAllHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateAll) + res, height, err := cliCtx.Query(route) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // parse res + var resultData []types.Model + err = json.Unmarshal(res, &resultData) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, resultData) + } +} + +func queryContractStateRawHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + decoder := newArgDecoder(hex.DecodeString) + addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + decoder.encoding = mux.Vars(r)["encoding"] + queryData, err := decoder.DecodeString(mux.Vars(r)["key"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateRaw) + res, height, err := cliCtx.QueryWithData(route, queryData) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx = cliCtx.WithHeight(height) + // ensure this is base64 encoded + encoded := base64.StdEncoding.EncodeToString(res) + rest.PostProcessResponse(w, cliCtx, encoded) + } +} + +type smartResponse struct { + Smart []byte `json:"smart"` +} + +func queryContractStateSmartHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + decoder := newArgDecoder(hex.DecodeString) + addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + decoder.encoding = mux.Vars(r)["encoding"] + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateSmart) + + queryData, err := decoder.DecodeString(mux.Vars(r)["query"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + res, height, err := cliCtx.QueryWithData(route, queryData) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + // return as raw bytes (to be base64-encoded) + responseData := smartResponse{Smart: res} + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, responseData) + } +} + +func queryContractHistoryFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + addr, err := sdk.AccAddressFromBech32(mux.Vars(r)["contractAddr"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryContractHistory, addr.String()) + res, height, err := cliCtx.Query(route) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, json.RawMessage(res)) + } +} + +type argumentDecoder struct { + // dec is the default decoder + dec func(string) ([]byte, error) + encoding string +} + +func newArgDecoder(def func(string) ([]byte, error)) *argumentDecoder { + return &argumentDecoder{dec: def} +} + +func (a *argumentDecoder) DecodeString(s string) ([]byte, error) { + switch a.encoding { + case "hex": + return hex.DecodeString(s) + case "base64": + return base64.StdEncoding.DecodeString(s) + default: + return a.dec(s) + } +} diff --git a/x/wasm/client/rest/rest.go b/x/wasm/client/rest/rest.go new file mode 100644 index 00000000..95339d3a --- /dev/null +++ b/x/wasm/client/rest/rest.go @@ -0,0 +1,15 @@ +// Deprecated: the rest package will be removed. You can use the GRPC gateway instead +package rest + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/gorilla/mux" +) + +// RegisterRoutes registers staking-related REST handlers to a router +// Deprecated: the rest package will be removed. You can use the GRPC gateway instead +func RegisterRoutes(cliCtx client.Context, r *mux.Router) { + registerQueryRoutes(cliCtx, r) + registerTxRoutes(cliCtx, r) + registerNewTxRoutes(cliCtx, r) +} diff --git a/x/wasm/client/rest/tx.go b/x/wasm/client/rest/tx.go new file mode 100644 index 00000000..7be7d67d --- /dev/null +++ b/x/wasm/client/rest/tx.go @@ -0,0 +1,149 @@ +package rest + +import ( + "net/http" + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/gorilla/mux" + + "github.com/cerc-io/laconicd/x/wasm/ioutils" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func registerTxRoutes(cliCtx client.Context, r *mux.Router) { + r.HandleFunc("/wasm/code", storeCodeHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/wasm/code/{codeId}", instantiateContractHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/wasm/contract/{contractAddr}", executeContractHandlerFn(cliCtx)).Methods("POST") +} + +type storeCodeReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + WasmBytes []byte `json:"wasm_bytes"` +} + +type instantiateContractReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Label string `json:"label" yaml:"label"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + Admin string `json:"admin,omitempty" yaml:"admin"` + Msg []byte `json:"msg" yaml:"msg"` +} + +type executeContractReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + ExecMsg []byte `json:"exec_msg" yaml:"exec_msg"` + Amount sdk.Coins `json:"coins" yaml:"coins"` +} + +func storeCodeHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req storeCodeReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + var err error + wasm := req.WasmBytes + + // gzip the wasm file + if ioutils.IsWasm(wasm) { + wasm, err = ioutils.GzipIt(wasm) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + } else if !ioutils.IsGzip(wasm) { + rest.WriteErrorResponse(w, http.StatusBadRequest, "Invalid input file, use wasm binary or zip") + return + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgStoreCode{ + Sender: req.BaseReq.From, + WASMByteCode: wasm, + } + + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, &msg) + } +} + +func instantiateContractHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req instantiateContractReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + vars := mux.Vars(r) + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // get the id of the code to instantiate + codeID, err := strconv.ParseUint(vars["codeId"], 10, 64) + if err != nil { + return + } + + msg := types.MsgInstantiateContract{ + Sender: req.BaseReq.From, + CodeID: codeID, + Label: req.Label, + Funds: req.Deposit, + Msg: req.Msg, + Admin: req.Admin, + } + + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, &msg) + } +} + +func executeContractHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req executeContractReq + if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + msg := types.MsgExecuteContract{ + Sender: req.BaseReq.From, + Contract: contractAddr, + Msg: req.ExecMsg, + Funds: req.Amount, + } + + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, &msg) + } +} diff --git a/x/wasm/common_test.go b/x/wasm/common_test.go new file mode 100644 index 00000000..ce232a5e --- /dev/null +++ b/x/wasm/common_test.go @@ -0,0 +1,34 @@ +package wasm + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +// ensure store code returns the expected response +func assertStoreCodeResponse(t *testing.T, data []byte, expected uint64) { + var pStoreResp MsgStoreCodeResponse + require.NoError(t, pStoreResp.Unmarshal(data)) + require.Equal(t, pStoreResp.CodeID, expected) +} + +// ensure execution returns the expected data +func assertExecuteResponse(t *testing.T, data []byte, expected []byte) { + var pExecResp 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 MsgInstantiateContractResponse + require.NoError(t, pInstResp.Unmarshal(data)) + require.NotEmpty(t, pInstResp.Address) + addr := pInstResp.Address + // ensure this is a valid sdk address + _, err := sdk.AccAddressFromBech32(addr) + require.NoError(t, err) + return addr +} diff --git a/x/wasm/genesis_test.go b/x/wasm/genesis_test.go new file mode 100644 index 00000000..9d968f87 --- /dev/null +++ b/x/wasm/genesis_test.go @@ -0,0 +1,96 @@ +package wasm + +import ( + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestInitGenesis(t *testing.T) { + data := setupTest(t) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := data.faucet.NewFundedRandomAccount(data.ctx, deposit.Add(deposit...)...) + fred := data.faucet.NewFundedRandomAccount(data.ctx, topUp...) + + h := data.module.Route().Handler() + q := data.module.LegacyQuerierHandler(nil) + + msg := MsgStoreCode{ + Sender: creator.String(), + WASMByteCode: testContract, + } + err := msg.ValidateBasic() + require.NoError(t, err) + + res, err := h(data.ctx, &msg) + require.NoError(t, err) + assertStoreCodeResponse(t, res.Data, 1) + + _, _, bob := keyPubAddr() + initMsg := initMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator.String(), + CodeID: firstCodeID, + Msg: initMsgBz, + Funds: deposit, + Label: "testing", + } + res, err = h(data.ctx, &initCmd) + require.NoError(t, err) + contractBech32Addr := parseInitResponse(t, res.Data) + + execCmd := MsgExecuteContract{ + Sender: fred.String(), + Contract: contractBech32Addr, + Msg: []byte(`{"release":{}}`), + Funds: topUp, + } + res, err = h(data.ctx, &execCmd) + require.NoError(t, err) + // from https://github.com/CosmWasm/cosmwasm/blob/master/contracts/hackatom/src/contract.rs#L167 + assertExecuteResponse(t, res.Data, []byte{0xf0, 0x0b, 0xaa}) + + // 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, 1, []string{contractBech32Addr}) + assertContractInfo(t, q, data.ctx, contractBech32Addr, 1, creator) + assertContractState(t, q, data.ctx, contractBech32Addr, state{ + Verifier: fred.String(), + Beneficiary: bob.String(), + Funder: creator.String(), + }) + + // export into genstate + genState := ExportGenesis(data.ctx, &data.keeper) + + // create new app to import genstate into + newData := setupTest(t) + q2 := newData.module.LegacyQuerierHandler(nil) + + // initialize new app with genstate + InitGenesis(newData.ctx, &newData.keeper, *genState) + + // run same checks again on newdata, to make sure it was reinitialized correctly + assertCodeList(t, q2, newData.ctx, 1) + assertCodeBytes(t, q2, newData.ctx, 1, testContract) + + assertContractList(t, q2, newData.ctx, 1, []string{contractBech32Addr}) + assertContractInfo(t, q2, newData.ctx, contractBech32Addr, 1, creator) + assertContractState(t, q2, newData.ctx, contractBech32Addr, state{ + Verifier: fred.String(), + Beneficiary: bob.String(), + Funder: creator.String(), + }) +} diff --git a/x/wasm/handler.go b/x/wasm/handler.go new file mode 100644 index 00000000..75cd080f --- /dev/null +++ b/x/wasm/handler.go @@ -0,0 +1,77 @@ +package wasm + +import ( + "fmt" + + "github.com/gogo/protobuf/proto" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// NewHandler returns a handler for "wasm" type messages. +func NewHandler(k types.ContractOpsKeeper) sdk.Handler { + msgServer := keeper.NewMsgServerImpl(k) + + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + var ( + res proto.Message + err error + ) + switch msg := msg.(type) { + case *MsgStoreCode: //nolint:typecheck + res, err = msgServer.StoreCode(sdk.WrapSDKContext(ctx), msg) + case *MsgInstantiateContract: + res, err = msgServer.InstantiateContract(sdk.WrapSDKContext(ctx), msg) + case *MsgInstantiateContract2: + res, err = msgServer.InstantiateContract2(sdk.WrapSDKContext(ctx), msg) + case *MsgExecuteContract: + res, err = msgServer.ExecuteContract(sdk.WrapSDKContext(ctx), msg) + case *MsgMigrateContract: + res, err = msgServer.MigrateContract(sdk.WrapSDKContext(ctx), msg) + case *MsgUpdateAdmin: + res, err = msgServer.UpdateAdmin(sdk.WrapSDKContext(ctx), msg) + case *MsgClearAdmin: + res, err = msgServer.ClearAdmin(sdk.WrapSDKContext(ctx), msg) + case *types.MsgUpdateInstantiateConfig: + res, err = msgServer.UpdateInstantiateConfig(sdk.WrapSDKContext(ctx), msg) + default: + errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg) + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg) + } + + ctx = ctx.WithEventManager(filterMessageEvents(ctx)) + return sdk.WrapServiceResult(ctx, res, err) + } +} + +// filterMessageEvents returns the same events with all of type == EventTypeMessage removed except +// for wasm message types. +// this is so only our top-level message event comes through +func filterMessageEvents(ctx sdk.Context) *sdk.EventManager { + m := sdk.NewEventManager() + for _, e := range ctx.EventManager().Events() { + if e.Type == sdk.EventTypeMessage && + !hasWasmModuleAttribute(e.Attributes) { + continue + } + m.EmitEvent(e) + } + return m +} + +func hasWasmModuleAttribute(attrs []abci.EventAttribute) bool { + for _, a := range attrs { + if sdk.AttributeKeyModule == string(a.Key) && + types.ModuleName == string(a.Value) { + return true + } + } + return false +} diff --git a/x/wasm/ibc.go b/x/wasm/ibc.go new file mode 100644 index 00000000..a20f6890 --- /dev/null +++ b/x/wasm/ibc.go @@ -0,0 +1,357 @@ +package wasm + +import ( + "math" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types" + host "github.com/cosmos/ibc-go/v4/modules/core/24-host" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ porttypes.IBCModule = IBCHandler{} + +// internal interface that is implemented by ibc middleware +type appVersionGetter interface { + // GetAppVersion returns the application level version with all middleware data stripped out + GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) +} + +type IBCHandler struct { + keeper types.IBCContractKeeper + channelKeeper types.ChannelKeeper + appVersionGetter appVersionGetter +} + +func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, vg appVersionGetter) IBCHandler { + return IBCHandler{keeper: k, channelKeeper: ck, appVersionGetter: vg} +} + +// OnChanOpenInit implements the IBCModule interface +func (i IBCHandler) OnChanOpenInit( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID string, + channelID string, + chanCap *capabilitytypes.Capability, + counterParty channeltypes.Counterparty, + version string, +) (string, error) { + // ensure port, version, capability + if err := ValidateChannelParams(channelID); err != nil { + return "", err + } + contractAddr, err := ContractFromPortID(portID) + if err != nil { + return "", sdkerrors.Wrapf(err, "contract port id") + } + + msg := wasmvmtypes.IBCChannelOpenMsg{ + OpenInit: &wasmvmtypes.IBCOpenInit{ + Channel: wasmvmtypes.IBCChannel{ + Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID}, + CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: counterParty.PortId, ChannelID: counterParty.ChannelId}, + Order: order.String(), + // DESIGN V3: this may be "" ?? + Version: version, + ConnectionID: connectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported. + }, + }, + } + + // Allow contracts to return a version (or default to proposed version if unset) + acceptedVersion, err := i.keeper.OnOpenChannel(ctx, contractAddr, msg) + if err != nil { + return "", err + } + if acceptedVersion == "" { + acceptedVersion = version + } + + // Claim channel capability passed back by IBC module + if err := i.keeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil { + return "", sdkerrors.Wrap(err, "claim capability") + } + return acceptedVersion, nil +} + +// OnChanOpenTry implements the IBCModule interface +func (i IBCHandler) OnChanOpenTry( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID, channelID string, + chanCap *capabilitytypes.Capability, + counterParty channeltypes.Counterparty, + counterpartyVersion string, +) (string, error) { + // ensure port, version, capability + if err := ValidateChannelParams(channelID); err != nil { + return "", err + } + + contractAddr, err := ContractFromPortID(portID) + if err != nil { + return "", sdkerrors.Wrapf(err, "contract port id") + } + + msg := wasmvmtypes.IBCChannelOpenMsg{ + OpenTry: &wasmvmtypes.IBCOpenTry{ + Channel: wasmvmtypes.IBCChannel{ + Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID}, + CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: counterParty.PortId, ChannelID: counterParty.ChannelId}, + Order: order.String(), + Version: counterpartyVersion, + ConnectionID: connectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported. + }, + CounterpartyVersion: counterpartyVersion, + }, + } + + // Allow contracts to return a version (or default to counterpartyVersion if unset) + version, err := i.keeper.OnOpenChannel(ctx, contractAddr, msg) + if err != nil { + return "", err + } + if version == "" { + version = counterpartyVersion + } + + // Module may have already claimed capability in OnChanOpenInit in the case of crossing hellos + // (ie chainA and chainB both call ChanOpenInit before one of them calls ChanOpenTry) + // If module can already authenticate the capability then module already owns it, so we don't need to claim + // Otherwise, module does not have channel capability, and we must claim it from IBC + if !i.keeper.AuthenticateCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)) { + // Only claim channel capability passed back by IBC module if we do not already own it + if err := i.keeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil { + return "", sdkerrors.Wrap(err, "claim capability") + } + } + + return version, nil +} + +// OnChanOpenAck implements the IBCModule interface +func (i IBCHandler) OnChanOpenAck( + ctx sdk.Context, + portID, channelID string, + counterpartyChannelID string, + counterpartyVersion string, +) error { + contractAddr, err := ContractFromPortID(portID) + if err != nil { + return sdkerrors.Wrapf(err, "contract port id") + } + channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID) + } + channelInfo.Counterparty.ChannelId = counterpartyChannelID + + appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID) + } + + msg := wasmvmtypes.IBCChannelConnectMsg{ + OpenAck: &wasmvmtypes.IBCOpenAck{ + Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion), + CounterpartyVersion: counterpartyVersion, + }, + } + return i.keeper.OnConnectChannel(ctx, contractAddr, msg) +} + +// OnChanOpenConfirm implements the IBCModule interface +func (i IBCHandler) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error { + contractAddr, err := ContractFromPortID(portID) + if err != nil { + return sdkerrors.Wrapf(err, "contract port id") + } + channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID) + } + appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID) + } + msg := wasmvmtypes.IBCChannelConnectMsg{ + OpenConfirm: &wasmvmtypes.IBCOpenConfirm{ + Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion), + }, + } + return i.keeper.OnConnectChannel(ctx, contractAddr, msg) +} + +// OnChanCloseInit implements the IBCModule interface +func (i IBCHandler) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error { + contractAddr, err := ContractFromPortID(portID) + if err != nil { + return sdkerrors.Wrapf(err, "contract port id") + } + channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID) + } + appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID) + } + + msg := wasmvmtypes.IBCChannelCloseMsg{ + CloseInit: &wasmvmtypes.IBCCloseInit{Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion)}, + } + err = i.keeper.OnCloseChannel(ctx, contractAddr, msg) + if err != nil { + return err + } + // emit events? + + return err +} + +// OnChanCloseConfirm implements the IBCModule interface +func (i IBCHandler) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error { + // counterparty has closed the channel + contractAddr, err := ContractFromPortID(portID) + if err != nil { + return sdkerrors.Wrapf(err, "contract port id") + } + channelInfo, ok := i.channelKeeper.GetChannel(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID) + } + appVersion, ok := i.appVersionGetter.GetAppVersion(ctx, portID, channelID) + if !ok { + return sdkerrors.Wrapf(channeltypes.ErrInvalidChannelVersion, "port ID (%s) channel ID (%s)", portID, channelID) + } + + msg := wasmvmtypes.IBCChannelCloseMsg{ + CloseConfirm: &wasmvmtypes.IBCCloseConfirm{Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion)}, + } + err = i.keeper.OnCloseChannel(ctx, contractAddr, msg) + if err != nil { + return err + } + // emit events? + + return err +} + +func toWasmVMChannel(portID, channelID string, channelInfo channeltypes.Channel, appVersion string) wasmvmtypes.IBCChannel { + return wasmvmtypes.IBCChannel{ + Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID}, + CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: channelInfo.Counterparty.PortId, ChannelID: channelInfo.Counterparty.ChannelId}, + Order: channelInfo.Ordering.String(), + Version: appVersion, + ConnectionID: channelInfo.ConnectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported. + } +} + +// OnRecvPacket implements the IBCModule interface +func (i IBCHandler) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) ibcexported.Acknowledgement { + contractAddr, err := ContractFromPortID(packet.DestinationPort) + if err != nil { + return channeltypes.NewErrorAcknowledgement(sdkerrors.Wrapf(err, "contract port id")) + } + msg := wasmvmtypes.IBCPacketReceiveMsg{Packet: newIBCPacket(packet), Relayer: relayer.String()} + ack, err := i.keeper.OnRecvPacket(ctx, contractAddr, msg) + if err != nil { + return channeltypes.NewErrorAcknowledgement(err) + } + return ContractConfirmStateAck(ack) +} + +var _ ibcexported.Acknowledgement = ContractConfirmStateAck{} + +type ContractConfirmStateAck []byte + +func (w ContractConfirmStateAck) Success() bool { + return true // always commit state +} + +func (w ContractConfirmStateAck) Acknowledgement() []byte { + return w +} + +// OnAcknowledgementPacket implements the IBCModule interface +func (i IBCHandler) OnAcknowledgementPacket( + ctx sdk.Context, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + contractAddr, err := ContractFromPortID(packet.SourcePort) + if err != nil { + return sdkerrors.Wrapf(err, "contract port id") + } + + err = i.keeper.OnAckPacket(ctx, contractAddr, wasmvmtypes.IBCPacketAckMsg{ + Acknowledgement: wasmvmtypes.IBCAcknowledgement{Data: acknowledgement}, + OriginalPacket: newIBCPacket(packet), + Relayer: relayer.String(), + }) + if err != nil { + return sdkerrors.Wrap(err, "on ack") + } + return nil +} + +// OnTimeoutPacket implements the IBCModule interface +func (i IBCHandler) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error { + contractAddr, err := ContractFromPortID(packet.SourcePort) + if err != nil { + return sdkerrors.Wrapf(err, "contract port id") + } + msg := wasmvmtypes.IBCPacketTimeoutMsg{Packet: newIBCPacket(packet), Relayer: relayer.String()} + err = i.keeper.OnTimeoutPacket(ctx, contractAddr, msg) + if err != nil { + return sdkerrors.Wrap(err, "on timeout") + } + return nil +} + +func newIBCPacket(packet channeltypes.Packet) wasmvmtypes.IBCPacket { + timeout := wasmvmtypes.IBCTimeout{ + Timestamp: packet.TimeoutTimestamp, + } + if !packet.TimeoutHeight.IsZero() { + timeout.Block = &wasmvmtypes.IBCTimeoutBlock{ + Height: packet.TimeoutHeight.RevisionHeight, + Revision: packet.TimeoutHeight.RevisionNumber, + } + } + + return wasmvmtypes.IBCPacket{ + Data: packet.Data, + Src: wasmvmtypes.IBCEndpoint{ChannelID: packet.SourceChannel, PortID: packet.SourcePort}, + Dest: wasmvmtypes.IBCEndpoint{ChannelID: packet.DestinationChannel, PortID: packet.DestinationPort}, + Sequence: packet.Sequence, + Timeout: timeout, + } +} + +func ValidateChannelParams(channelID string) error { + // NOTE: for escrow address security only 2^32 channels are allowed to be created + // Issue: https://github.com/cosmos/cosmos-sdk/issues/7737 + channelSequence, err := channeltypes.ParseChannelSequence(channelID) + if err != nil { + return err + } + if channelSequence > math.MaxUint32 { + return sdkerrors.Wrapf(types.ErrMaxIBCChannels, "channel sequence %d is greater than max allowed transfer channels %d", channelSequence, math.MaxUint32) + } + return nil +} diff --git a/x/wasm/ibc_integration_test.go b/x/wasm/ibc_integration_test.go new file mode 100644 index 00000000..2cdf0193 --- /dev/null +++ b/x/wasm/ibc_integration_test.go @@ -0,0 +1,126 @@ +package wasm_test + +import ( + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + wasmibctesting "github.com/cerc-io/laconicd/x/wasm/ibctesting" + wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" +) + +func TestOnChanOpenInitVersion(t *testing.T) { + const startVersion = "v1" + specs := map[string]struct { + contractRsp *wasmvmtypes.IBC3ChannelOpenResponse + expVersion string + }{ + "different version": { + contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{Version: "v2"}, + expVersion: "v2", + }, + "no response": { + expVersion: startVersion, + }, + "empty result": { + contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{}, + expVersion: startVersion, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myContract := &wasmtesting.MockIBCContractCallbacks{ + IBCChannelOpenFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + return spec.contractRsp, 0, nil + }, + } + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContract)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts) + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + myContractAddr = chainA.SeedNewContractInstance() + contractInfo = chainA.App.WasmKeeper.GetContractInfo(chainA.GetContext(), myContractAddr) + ) + + path := wasmibctesting.NewPath(chainA, chainB) + coordinator.SetupConnections(path) + + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: contractInfo.IBCPortID, + Version: startVersion, + Order: channeltypes.UNORDERED, + } + require.NoError(t, path.EndpointA.ChanOpenInit()) + assert.Equal(t, spec.expVersion, path.EndpointA.ChannelConfig.Version) + }) + } +} + +func TestOnChanOpenTryVersion(t *testing.T) { + const startVersion = ibctransfertypes.Version + specs := map[string]struct { + contractRsp *wasmvmtypes.IBC3ChannelOpenResponse + expVersion string + }{ + "different version": { + contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{Version: "v2"}, + expVersion: "v2", + }, + "no response": { + expVersion: startVersion, + }, + "empty result": { + contractRsp: &wasmvmtypes.IBC3ChannelOpenResponse{}, + expVersion: startVersion, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myContract := &wasmtesting.MockIBCContractCallbacks{ + IBCChannelOpenFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + return spec.contractRsp, 0, nil + }, + } + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContract)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts) + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + myContractAddr = chainA.SeedNewContractInstance() + contractInfo = chainA.ContractInfo(myContractAddr) + ) + + path := wasmibctesting.NewPath(chainA, chainB) + coordinator.SetupConnections(path) + + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: contractInfo.IBCPortID, + Version: startVersion, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: ibctransfertypes.PortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + + require.NoError(t, path.EndpointB.ChanOpenInit()) + require.NoError(t, path.EndpointA.ChanOpenTry()) + assert.Equal(t, spec.expVersion, path.EndpointA.ChannelConfig.Version) + }) + } +} diff --git a/x/wasm/ibc_reflect_test.go b/x/wasm/ibc_reflect_test.go new file mode 100644 index 00000000..31e3e465 --- /dev/null +++ b/x/wasm/ibc_reflect_test.go @@ -0,0 +1,124 @@ +package wasm_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/stretchr/testify/require" + + wasmibctesting "github.com/cerc-io/laconicd/x/wasm/ibctesting" + wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper" +) + +func TestIBCReflectContract(t *testing.T) { + // scenario: + // chain A: ibc_reflect_send.wasm + // chain B: reflect.wasm + ibc_reflect.wasm + // + // Chain A "ibc_reflect_send" sends a IBC packet "on channel connect" event to chain B "ibc_reflect" + // "ibc_reflect" sends a submessage to "reflect" which is returned as submessage. + + var ( + coordinator = wasmibctesting.NewCoordinator(t, 2) + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + coordinator.CommitBlock(chainA, chainB) + + initMsg := []byte(`{}`) + codeID := chainA.StoreCodeFile("./keeper/testdata/ibc_reflect_send.wasm").CodeID + sendContractAddr := chainA.InstantiateContract(codeID, initMsg) + + reflectID := chainB.StoreCodeFile("./keeper/testdata/reflect.wasm").CodeID + initMsg = wasmkeeper.IBCReflectInitMsg{ + ReflectCodeID: reflectID, + }.GetBytes(t) + codeID = chainB.StoreCodeFile("./keeper/testdata/ibc_reflect.wasm").CodeID + + reflectContractAddr := chainB.InstantiateContract(codeID, initMsg) + var ( + sourcePortID = chainA.ContractInfo(sendContractAddr).IBCPortID + counterpartPortID = chainB.ContractInfo(reflectContractAddr).IBCPortID + ) + coordinator.CommitBlock(chainA, chainB) + coordinator.UpdateTime() + + require.Equal(t, chainA.CurrentHeader.Time, chainB.CurrentHeader.Time) + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: sourcePortID, + Version: "ibc-reflect-v1", + Order: channeltypes.ORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: counterpartPortID, + Version: "ibc-reflect-v1", + Order: channeltypes.ORDERED, + } + + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + // TODO: query both contracts directly to ensure they have registered the proper connection + // (and the chainB has created a reflect contract) + + // there should be one packet to relay back and forth (whoami) + // TODO: how do I find the packet that was previously sent by the smart contract? + // Coordinator.RecvPacket requires channeltypes.Packet as input? + // Given the source (portID, channelID), we should be able to count how many packets are pending, query the data + // and submit them to the other side (same with acks). This is what the real relayer does. I guess the test framework doesn't? + + // Update: I dug through the code, especially channel.Keeper.SendPacket, and it only writes a commitment + // only writes I see: https://github.com/cosmos/cosmos-sdk/blob/31fdee0228bd6f3e787489c8e4434aabc8facb7d/x/ibc/core/04-channel/keeper/packet.go#L115-L116 + // commitment is hashed packet: https://github.com/cosmos/cosmos-sdk/blob/31fdee0228bd6f3e787489c8e4434aabc8facb7d/x/ibc/core/04-channel/types/packet.go#L14-L34 + // how is the relayer supposed to get the original packet data?? + // eg. ibctransfer doesn't store the packet either: https://github.com/cosmos/cosmos-sdk/blob/master/x/ibc/applications/transfer/keeper/relay.go#L145-L162 + // ... or I guess the original packet data is only available in the event logs???? + // https://github.com/cosmos/cosmos-sdk/blob/31fdee0228bd6f3e787489c8e4434aabc8facb7d/x/ibc/core/04-channel/keeper/packet.go#L121-L132 + + // ensure the expected packet was prepared, and relay it + require.Equal(t, 1, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + err := coordinator.RelayAndAckPendingPackets(path) + require.NoError(t, err) + require.Equal(t, 0, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // let's query the source contract and make sure it registered an address + query := ReflectSendQueryMsg{Account: &AccountQuery{ChannelID: path.EndpointA.ChannelID}} + var account AccountResponse + err = chainA.SmartQuery(sendContractAddr.String(), query, &account) + require.NoError(t, err) + require.NotEmpty(t, account.RemoteAddr) + require.Empty(t, account.RemoteBalance) + + // close channel + coordinator.CloseChannel(path) + + // let's query the source contract and make sure it registered an address + account = AccountResponse{} + err = chainA.SmartQuery(sendContractAddr.String(), query, &account) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +type ReflectSendQueryMsg struct { + Admin *struct{} `json:"admin,omitempty"` + ListAccounts *struct{} `json:"list_accounts,omitempty"` + Account *AccountQuery `json:"account,omitempty"` +} + +type AccountQuery struct { + ChannelID string `json:"channel_id"` +} + +type AccountResponse struct { + LastUpdateTime uint64 `json:"last_update_time,string"` + RemoteAddr string `json:"remote_addr"` + RemoteBalance wasmvmtypes.Coins `json:"remote_balance"` +} diff --git a/x/wasm/ibc_test.go b/x/wasm/ibc_test.go new file mode 100644 index 00000000..ee63c7fb --- /dev/null +++ b/x/wasm/ibc_test.go @@ -0,0 +1,82 @@ +package wasm + +import ( + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + "github.com/stretchr/testify/assert" +) + +func TestMapToWasmVMIBCPacket(t *testing.T) { + var myTimestamp uint64 = 1 + specs := map[string]struct { + src channeltypes.Packet + exp wasmvmtypes.IBCPacket + }{ + "with height timeout": { + src: IBCPacketFixture(), + exp: wasmvmtypes.IBCPacket{ + Data: []byte("myData"), + Src: wasmvmtypes.IBCEndpoint{PortID: "srcPort", ChannelID: "channel-1"}, + Dest: wasmvmtypes.IBCEndpoint{PortID: "destPort", ChannelID: "channel-2"}, + Sequence: 1, + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2}}, + }, + }, + "with time timeout": { + src: IBCPacketFixture(func(p *channeltypes.Packet) { + p.TimeoutTimestamp = myTimestamp + p.TimeoutHeight = clienttypes.Height{} + }), + exp: wasmvmtypes.IBCPacket{ + Data: []byte("myData"), + Src: wasmvmtypes.IBCEndpoint{PortID: "srcPort", ChannelID: "channel-1"}, + Dest: wasmvmtypes.IBCEndpoint{PortID: "destPort", ChannelID: "channel-2"}, + Sequence: 1, + Timeout: wasmvmtypes.IBCTimeout{Timestamp: myTimestamp}, + }, + }, "with time and height timeout": { + src: IBCPacketFixture(func(p *channeltypes.Packet) { + p.TimeoutTimestamp = myTimestamp + }), + exp: wasmvmtypes.IBCPacket{ + Data: []byte("myData"), + Src: wasmvmtypes.IBCEndpoint{PortID: "srcPort", ChannelID: "channel-1"}, + Dest: wasmvmtypes.IBCEndpoint{PortID: "destPort", ChannelID: "channel-2"}, + Sequence: 1, + Timeout: wasmvmtypes.IBCTimeout{ + Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2}, + Timestamp: myTimestamp, + }, + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := newIBCPacket(spec.src) + assert.Equal(t, spec.exp, got) + }) + } +} + +func IBCPacketFixture(mutators ...func(p *channeltypes.Packet)) channeltypes.Packet { + r := channeltypes.Packet{ + Sequence: 1, + SourcePort: "srcPort", + SourceChannel: "channel-1", + DestinationPort: "destPort", + DestinationChannel: "channel-2", + Data: []byte("myData"), + TimeoutHeight: clienttypes.Height{ + RevisionHeight: 1, + RevisionNumber: 2, + }, + TimeoutTimestamp: 0, + } + for _, m := range mutators { + m(&r) + } + return r +} diff --git a/x/wasm/ibctesting/README.md b/x/wasm/ibctesting/README.md new file mode 100644 index 00000000..1c928699 --- /dev/null +++ b/x/wasm/ibctesting/README.md @@ -0,0 +1,2 @@ +# testing package for ibc +Customized version of cosmos-sdk x/ibc/testing \ No newline at end of file diff --git a/x/wasm/ibctesting/chain.go b/x/wasm/ibctesting/chain.go new file mode 100644 index 00000000..e127a9db --- /dev/null +++ b/x/wasm/ibctesting/chain.go @@ -0,0 +1,594 @@ +package ibctesting + +import ( + "fmt" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + commitmenttypes "github.com/cosmos/ibc-go/v4/modules/core/23-commitment/types" + host "github.com/cosmos/ibc-go/v4/modules/core/24-host" + "github.com/cosmos/ibc-go/v4/modules/core/exported" + "github.com/cosmos/ibc-go/v4/modules/core/types" + ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + "github.com/cosmos/ibc-go/v4/testing/mock" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/tmhash" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmprotoversion "github.com/tendermint/tendermint/proto/tendermint/version" + tmtypes "github.com/tendermint/tendermint/types" + tmversion "github.com/tendermint/tendermint/version" + + "github.com/cerc-io/laconicd/app" + "github.com/cerc-io/laconicd/app/params" + "github.com/cerc-io/laconicd/x/wasm" +) + +var MaxAccounts = 10 + +type SenderAccount struct { + SenderPrivKey cryptotypes.PrivKey + SenderAccount authtypes.AccountI +} + +// TestChain is a testing struct that wraps a simapp with the last TM Header, the current ABCI +// header and the validators of the TestChain. It also contains a field called ChainID. This +// is the clientID that *other* chains use to refer to this TestChain. The SenderAccount +// is used for delivering transactions through the application state. +// NOTE: the actual application uses an empty chain-id for ease of testing. +type TestChain struct { + t *testing.T + + Coordinator *Coordinator + App *app.WasmApp + ChainID string + LastHeader *ibctmtypes.Header // header for last block height committed + CurrentHeader tmproto.Header // header for current block height + QueryServer types.QueryServer + TxConfig client.TxConfig + Codec codec.BinaryCodec + + Vals *tmtypes.ValidatorSet + NextVals *tmtypes.ValidatorSet + + // Signers is a map from validator address to the PrivValidator + // The map is converted into an array that is the same order as the validators right before signing commit + // This ensures that signers will always be in correct order even as validator powers change. + // If a test adds a new validator after chain creation, then the signer map must be updated to include + // the new PrivValidator entry. + Signers map[string]tmtypes.PrivValidator + + // autogenerated sender private key + SenderPrivKey cryptotypes.PrivKey + SenderAccount authtypes.AccountI + SenderAccounts []SenderAccount + + PendingSendPackets []channeltypes.Packet +} + +type PacketAck struct { + Packet channeltypes.Packet + Ack []byte +} + +// NewTestChain initializes a new test chain with a default of 4 validators +// Use this function if the tests do not need custom control over the validator set +func NewTestChain(t *testing.T, coord *Coordinator, chainID string, opts ...wasm.Option) *TestChain { + // generate validators private/public key + var ( + validatorsPerChain = 4 + validators = make([]*tmtypes.Validator, 0, validatorsPerChain) + signersByAddress = make(map[string]tmtypes.PrivValidator, validatorsPerChain) + ) + + for i := 0; i < validatorsPerChain; i++ { + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + validators = append(validators, tmtypes.NewValidator(pubKey, 1)) + signersByAddress[pubKey.Address().String()] = privVal + } + + // construct validator set; + // Note that the validators are sorted by voting power + // or, if equal, by address lexical order + valSet := tmtypes.NewValidatorSet(validators) + + return NewTestChainWithValSet(t, coord, chainID, valSet, signersByAddress, opts...) +} + +// NewTestChainWithValSet initializes a new TestChain instance with the given validator set +// and signer array. It also initializes 10 Sender accounts with a balance of 10000000000000000000 coins of +// bond denom to use for tests. +// +// The first block height is committed to state in order to allow for client creations on +// counterparty chains. The TestChain will return with a block height starting at 2. +// +// Time management is handled by the Coordinator in order to ensure synchrony between chains. +// Each update of any chain increments the block header time for all chains by 5 seconds. +// +// NOTE: to use a custom sender privkey and account for testing purposes, replace and modify this +// constructor function. +// +// CONTRACT: Validator array must be provided in the order expected by Tendermint. +// i.e. sorted first by power and then lexicographically by address. +func NewTestChainWithValSet(t *testing.T, coord *Coordinator, chainID string, valSet *tmtypes.ValidatorSet, signers map[string]tmtypes.PrivValidator, opts ...wasm.Option) *TestChain { + genAccs := []authtypes.GenesisAccount{} + genBals := []banktypes.Balance{} + senderAccs := []SenderAccount{} + + // generate genesis accounts + for i := 0; i < MaxAccounts; i++ { + senderPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), uint64(i), 0) + amount, ok := sdk.NewIntFromString("10000000000000000000") + require.True(t, ok) + + // add sender account + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)), + } + + genAccs = append(genAccs, acc) + genBals = append(genBals, balance) + + senderAcc := SenderAccount{ + SenderAccount: acc, + SenderPrivKey: senderPrivKey, + } + + senderAccs = append(senderAccs, senderAcc) + } + + wasmApp := app.SetupWithGenesisValSet(t, valSet, genAccs, chainID, opts, genBals...) + + // create current header and call begin block + header := tmproto.Header{ + ChainID: chainID, + Height: 1, + Time: coord.CurrentTime.UTC(), + } + + txConfig := params.MakeEncodingConfig().TxConfig + + // create an account to send transactions from + chain := &TestChain{ + t: t, + Coordinator: coord, + ChainID: chainID, + App: wasmApp, + CurrentHeader: header, + QueryServer: wasmApp.IBCKeeper, + TxConfig: txConfig, + Codec: wasmApp.AppCodec(), + Vals: valSet, + NextVals: valSet, + Signers: signers, + SenderPrivKey: senderAccs[0].SenderPrivKey, + SenderAccount: senderAccs[0].SenderAccount, + SenderAccounts: senderAccs, + } + + coord.CommitBlock(chain) + + return chain +} + +// GetContext returns the current context for the application. +func (chain *TestChain) GetContext() sdk.Context { + return chain.App.BaseApp.NewContext(false, chain.CurrentHeader) +} + +// QueryProof performs an abci query with the given key and returns the proto encoded merkle proof +// for the query and the height at which the proof will succeed on a tendermint verifier. +func (chain *TestChain) QueryProof(key []byte) ([]byte, clienttypes.Height) { + return chain.QueryProofAtHeight(key, chain.App.LastBlockHeight()) +} + +// QueryProofAtHeight performs an abci query with the given key and returns the proto encoded merkle proof +// for the query and the height at which the proof will succeed on a tendermint verifier. +func (chain *TestChain) QueryProofAtHeight(key []byte, height int64) ([]byte, clienttypes.Height) { + res := chain.App.Query(abci.RequestQuery{ + Path: fmt.Sprintf("store/%s/key", host.StoreKey), + Height: height - 1, + Data: key, + Prove: true, + }) + + merkleProof, err := commitmenttypes.ConvertProofs(res.ProofOps) + require.NoError(chain.t, err) + + proof, err := chain.App.AppCodec().Marshal(&merkleProof) + require.NoError(chain.t, err) + + revision := clienttypes.ParseChainID(chain.ChainID) + + // proof height + 1 is returned as the proof created corresponds to the height the proof + // was created in the IAVL tree. Tendermint and subsequently the clients that rely on it + // have heights 1 above the IAVL tree. Thus we return proof height + 1 + return proof, clienttypes.NewHeight(revision, uint64(res.Height)+1) +} + +// QueryUpgradeProof performs an abci query with the given key and returns the proto encoded merkle proof +// for the query and the height at which the proof will succeed on a tendermint verifier. +func (chain *TestChain) QueryUpgradeProof(key []byte, height uint64) ([]byte, clienttypes.Height) { + res := chain.App.Query(abci.RequestQuery{ + Path: "store/upgrade/key", + Height: int64(height - 1), + Data: key, + Prove: true, + }) + + merkleProof, err := commitmenttypes.ConvertProofs(res.ProofOps) + require.NoError(chain.t, err) + + proof, err := chain.App.AppCodec().Marshal(&merkleProof) + require.NoError(chain.t, err) + + revision := clienttypes.ParseChainID(chain.ChainID) + + // proof height + 1 is returned as the proof created corresponds to the height the proof + // was created in the IAVL tree. Tendermint and subsequently the clients that rely on it + // have heights 1 above the IAVL tree. Thus we return proof height + 1 + return proof, clienttypes.NewHeight(revision, uint64(res.Height+1)) +} + +// QueryConsensusStateProof performs an abci query for a consensus state +// stored on the given clientID. The proof and consensusHeight are returned. +func (chain *TestChain) QueryConsensusStateProof(clientID string) ([]byte, clienttypes.Height) { + clientState := chain.GetClientState(clientID) + + consensusHeight := clientState.GetLatestHeight().(clienttypes.Height) + consensusKey := host.FullConsensusStateKey(clientID, consensusHeight) + proofConsensus, _ := chain.QueryProof(consensusKey) + + return proofConsensus, consensusHeight +} + +// NextBlock sets the last header to the current header and increments the current header to be +// at the next block height. It does not update the time as that is handled by the Coordinator. +// It will call Endblock and Commit and apply the validator set changes to the next validators +// of the next block being created. This follows the Tendermint protocol of applying valset changes +// returned on block `n` to the validators of block `n+2`. +// It calls BeginBlock with the new block created before returning. +func (chain *TestChain) NextBlock() { + res := chain.App.EndBlock(abci.RequestEndBlock{Height: chain.CurrentHeader.Height}) + + chain.App.Commit() + + // set the last header to the current header + // use nil trusted fields + chain.LastHeader = chain.CurrentTMClientHeader() + + // val set changes returned from previous block get applied to the next validators + // of this block. See tendermint spec for details. + chain.Vals = chain.NextVals + chain.NextVals = ibctesting.ApplyValSetChanges(chain.t, chain.Vals, res.ValidatorUpdates) + + // increment the current header + chain.CurrentHeader = tmproto.Header{ + ChainID: chain.ChainID, + Height: chain.App.LastBlockHeight() + 1, + AppHash: chain.App.LastCommitID().Hash, + // NOTE: the time is increased by the coordinator to maintain time synchrony amongst + // chains. + Time: chain.CurrentHeader.Time, + ValidatorsHash: chain.Vals.Hash(), + NextValidatorsHash: chain.NextVals.Hash(), + } + + chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader}) +} + +// sendMsgs delivers a transaction through the application without returning the result. +func (chain *TestChain) sendMsgs(msgs ...sdk.Msg) error { + _, err := chain.SendMsgs(msgs...) + return err +} + +// SendMsgs delivers a transaction through the application. It updates the senders sequence +// number and updates the TestChain's headers. It returns the result and error if one +// occurred. +func (chain *TestChain) SendMsgs(msgs ...sdk.Msg) (*sdk.Result, error) { + // ensure the chain has the latest time + chain.Coordinator.UpdateTimeForChain(chain) + + _, r, err := app.SignAndDeliver( + chain.t, + chain.TxConfig, + chain.App.BaseApp, + chain.GetContext().BlockHeader(), + msgs, + chain.ChainID, + []uint64{chain.SenderAccount.GetAccountNumber()}, + []uint64{chain.SenderAccount.GetSequence()}, + chain.SenderPrivKey, + ) + + // NextBlock calls app.Commit() + chain.NextBlock() + if err != nil { + return r, err + } + + // increment sequence for successful transaction execution + err = chain.SenderAccount.SetSequence(chain.SenderAccount.GetSequence() + 1) + if err != nil { + return nil, err + } + + chain.Coordinator.IncrementTime() + + chain.captureIBCEvents(r) + + return r, nil +} + +func (chain *TestChain) captureIBCEvents(r *sdk.Result) { + toSend := getSendPackets(r.Events) + if len(toSend) > 0 { + // Keep a queue on the chain that we can relay in tests + chain.PendingSendPackets = append(chain.PendingSendPackets, toSend...) + } +} + +// GetClientState retrieves the client state for the provided clientID. The client is +// expected to exist otherwise testing will fail. +func (chain *TestChain) GetClientState(clientID string) exported.ClientState { + clientState, found := chain.App.IBCKeeper.ClientKeeper.GetClientState(chain.GetContext(), clientID) + require.True(chain.t, found) + + return clientState +} + +// GetConsensusState retrieves the consensus state for the provided clientID and height. +// It will return a success boolean depending on if consensus state exists or not. +func (chain *TestChain) GetConsensusState(clientID string, height exported.Height) (exported.ConsensusState, bool) { + return chain.App.IBCKeeper.ClientKeeper.GetClientConsensusState(chain.GetContext(), clientID, height) +} + +// GetValsAtHeight will return the validator set of the chain at a given height. It will return +// a success boolean depending on if the validator set exists or not at that height. +func (chain *TestChain) GetValsAtHeight(height int64) (*tmtypes.ValidatorSet, bool) { + histInfo, ok := chain.App.StakingKeeper.GetHistoricalInfo(chain.GetContext(), height) + if !ok { + return nil, false + } + + valSet := stakingtypes.Validators(histInfo.Valset) + + tmValidators, err := teststaking.ToTmValidators(valSet, sdk.DefaultPowerReduction) + if err != nil { + panic(err) + } + return tmtypes.NewValidatorSet(tmValidators), true +} + +// GetAcknowledgement retrieves an acknowledgement for the provided packet. If the +// acknowledgement does not exist then testing will fail. +func (chain *TestChain) GetAcknowledgement(packet exported.PacketI) []byte { + ack, found := chain.App.IBCKeeper.ChannelKeeper.GetPacketAcknowledgement(chain.GetContext(), packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + require.True(chain.t, found) + + return ack +} + +// GetPrefix returns the prefix for used by a chain in connection creation +func (chain *TestChain) GetPrefix() commitmenttypes.MerklePrefix { + return commitmenttypes.NewMerklePrefix(chain.App.IBCKeeper.ConnectionKeeper.GetCommitmentPrefix().Bytes()) +} + +// ConstructUpdateTMClientHeader will construct a valid 07-tendermint Header to update the +// light client on the source chain. +func (chain *TestChain) ConstructUpdateTMClientHeader(counterparty *TestChain, clientID string) (*ibctmtypes.Header, error) { + return chain.ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty, clientID, clienttypes.ZeroHeight()) +} + +// ConstructUpdateTMClientHeader will construct a valid 07-tendermint Header to update the +// light client on the source chain. +func (chain *TestChain) ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty *TestChain, clientID string, trustedHeight clienttypes.Height) (*ibctmtypes.Header, error) { + header := counterparty.LastHeader + // Relayer must query for LatestHeight on client to get TrustedHeight if the trusted height is not set + if trustedHeight.IsZero() { + trustedHeight = chain.GetClientState(clientID).GetLatestHeight().(clienttypes.Height) + } + var ( + tmTrustedVals *tmtypes.ValidatorSet + ok bool + ) + // Once we get TrustedHeight from client, we must query the validators from the counterparty chain + // If the LatestHeight == LastHeader.Height, then TrustedValidators are current validators + // If LatestHeight < LastHeader.Height, we can query the historical validator set from HistoricalInfo + if trustedHeight == counterparty.LastHeader.GetHeight() { + tmTrustedVals = counterparty.Vals + } else { + // NOTE: We need to get validators from counterparty at height: trustedHeight+1 + // since the last trusted validators for a header at height h + // is the NextValidators at h+1 committed to in header h by + // NextValidatorsHash + tmTrustedVals, ok = counterparty.GetValsAtHeight(int64(trustedHeight.RevisionHeight + 1)) + if !ok { + return nil, sdkerrors.Wrapf(ibctmtypes.ErrInvalidHeaderHeight, "could not retrieve trusted validators at trustedHeight: %d", trustedHeight) + } + } + // inject trusted fields into last header + // for now assume revision number is 0 + header.TrustedHeight = trustedHeight + + trustedVals, err := tmTrustedVals.ToProto() + if err != nil { + return nil, err + } + header.TrustedValidators = trustedVals + + return header, nil +} + +// ExpireClient fast forwards the chain's block time by the provided amount of time which will +// expire any clients with a trusting period less than or equal to this amount of time. +func (chain *TestChain) ExpireClient(amount time.Duration) { + chain.Coordinator.IncrementTimeBy(amount) +} + +// CurrentTMClientHeader creates a TM header using the current header parameters +// on the chain. The trusted fields in the header are set to nil. +func (chain *TestChain) CurrentTMClientHeader() *ibctmtypes.Header { + return chain.CreateTMClientHeader(chain.ChainID, chain.CurrentHeader.Height, clienttypes.Height{}, chain.CurrentHeader.Time, chain.Vals, chain.NextVals, nil, chain.Signers) +} + +// CreateTMClientHeader creates a TM header to update the TM client. Args are passed in to allow +// caller flexibility to use params that differ from the chain. +func (chain *TestChain) CreateTMClientHeader(chainID string, blockHeight int64, trustedHeight clienttypes.Height, timestamp time.Time, tmValSet, nextVals, tmTrustedVals *tmtypes.ValidatorSet, signers map[string]tmtypes.PrivValidator) *ibctmtypes.Header { + var ( + valSet *tmproto.ValidatorSet + trustedVals *tmproto.ValidatorSet + ) + require.NotNil(chain.t, tmValSet) + + vsetHash := tmValSet.Hash() + nextValHash := nextVals.Hash() + + tmHeader := tmtypes.Header{ + Version: tmprotoversion.Consensus{Block: tmversion.BlockProtocol, App: 2}, + ChainID: chainID, + Height: blockHeight, + Time: timestamp, + LastBlockID: MakeBlockID(make([]byte, tmhash.Size), 10_000, make([]byte, tmhash.Size)), + LastCommitHash: chain.App.LastCommitID().Hash, + DataHash: tmhash.Sum([]byte("data_hash")), + ValidatorsHash: vsetHash, + NextValidatorsHash: nextValHash, + ConsensusHash: tmhash.Sum([]byte("consensus_hash")), + AppHash: chain.CurrentHeader.AppHash, + LastResultsHash: tmhash.Sum([]byte("last_results_hash")), + EvidenceHash: tmhash.Sum([]byte("evidence_hash")), + ProposerAddress: tmValSet.Proposer.Address, //nolint:staticcheck + } + + hhash := tmHeader.Hash() + blockID := MakeBlockID(hhash, 3, tmhash.Sum([]byte("part_set"))) + voteSet := tmtypes.NewVoteSet(chainID, blockHeight, 1, tmproto.PrecommitType, tmValSet) + + // MakeCommit expects a signer array in the same order as the validator array. + // Thus we iterate over the ordered validator set and construct a signer array + // from the signer map in the same order. + signerArr := make([]tmtypes.PrivValidator, len(tmValSet.Validators)) + for i, v := range tmValSet.Validators { + signerArr[i] = signers[v.Address.String()] + } + + commit, err := tmtypes.MakeCommit(blockID, blockHeight, 1, voteSet, signerArr, timestamp) + require.NoError(chain.t, err) + + signedHeader := &tmproto.SignedHeader{ + Header: tmHeader.ToProto(), + Commit: commit.ToProto(), + } + + valSet, err = tmValSet.ToProto() + require.NoError(chain.t, err) + + if tmTrustedVals != nil { + trustedVals, err = tmTrustedVals.ToProto() + require.NoError(chain.t, err) + } + + // The trusted fields may be nil. They may be filled before relaying messages to a client. + // The relayer is responsible for querying client and injecting appropriate trusted fields. + return &ibctmtypes.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: trustedHeight, + TrustedValidators: trustedVals, + } +} + +// MakeBlockID copied unimported test functions from tmtypes to use them here +func MakeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) tmtypes.BlockID { + return tmtypes.BlockID{ + Hash: hash, + PartSetHeader: tmtypes.PartSetHeader{ + Total: partSetSize, + Hash: partSetHash, + }, + } +} + +// CreatePortCapability binds and claims a capability for the given portID if it does not +// already exist. This function will fail testing on any resulting error. +// NOTE: only creation of a capability for a transfer or mock port is supported +// Other applications must bind to the port in InitGenesis or modify this code. +func (chain *TestChain) CreatePortCapability(scopedKeeper capabilitykeeper.ScopedKeeper, portID string) { + // check if the portId is already binded, if not bind it + _, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), host.PortPath(portID)) + if !ok { + // create capability using the IBC capability keeper + cap, err := chain.App.ScopedIBCKeeper.NewCapability(chain.GetContext(), host.PortPath(portID)) + require.NoError(chain.t, err) + + // claim capability using the scopedKeeper + err = scopedKeeper.ClaimCapability(chain.GetContext(), cap, host.PortPath(portID)) + require.NoError(chain.t, err) + } + + chain.NextBlock() +} + +// GetPortCapability returns the port capability for the given portID. The capability must +// exist, otherwise testing will fail. +func (chain *TestChain) GetPortCapability(portID string) *capabilitytypes.Capability { + cap, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), host.PortPath(portID)) + require.True(chain.t, ok) + + return cap +} + +// CreateChannelCapability binds and claims a capability for the given portID and channelID +// if it does not already exist. This function will fail testing on any resulting error. The +// scoped keeper passed in will claim the new capability. +func (chain *TestChain) CreateChannelCapability(scopedKeeper capabilitykeeper.ScopedKeeper, portID, channelID string) { + capName := host.ChannelCapabilityPath(portID, channelID) + // check if the portId is already binded, if not bind it + _, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), capName) + if !ok { + cap, err := chain.App.ScopedIBCKeeper.NewCapability(chain.GetContext(), capName) + require.NoError(chain.t, err) + err = scopedKeeper.ClaimCapability(chain.GetContext(), cap, capName) + require.NoError(chain.t, err) + } + + chain.NextBlock() +} + +// GetChannelCapability returns the channel capability for the given portID and channelID. +// The capability must exist, otherwise testing will fail. +func (chain *TestChain) GetChannelCapability(portID, channelID string) *capabilitytypes.Capability { + cap, ok := chain.App.ScopedIBCKeeper.GetCapability(chain.GetContext(), host.ChannelCapabilityPath(portID, channelID)) + require.True(chain.t, ok) + + return cap +} + +func (chain *TestChain) Balance(acc sdk.AccAddress, denom string) sdk.Coin { + return chain.App.BankKeeper.GetBalance(chain.GetContext(), acc, denom) +} + +func (chain *TestChain) AllBalances(acc sdk.AccAddress) sdk.Coins { + return chain.App.BankKeeper.GetAllBalances(chain.GetContext(), acc) +} diff --git a/x/wasm/ibctesting/coordinator.go b/x/wasm/ibctesting/coordinator.go new file mode 100644 index 00000000..5e30d139 --- /dev/null +++ b/x/wasm/ibctesting/coordinator.go @@ -0,0 +1,317 @@ +package ibctesting + +import ( + "fmt" + "strconv" + "testing" + "time" + + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v4/modules/core/24-host" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper" +) + +const ChainIDPrefix = "testchain" + +var ( + globalStartTime = time.Date(2020, 12, 4, 10, 30, 0, 0, time.UTC) + TimeIncrement = time.Second * 5 +) + +// Coordinator is a testing struct which contains N TestChain's. It handles keeping all chains +// in sync with regards to time. +type Coordinator struct { + t *testing.T + + CurrentTime time.Time + Chains map[string]*TestChain +} + +// NewCoordinator initializes Coordinator with N TestChain's +func NewCoordinator(t *testing.T, n int, opts ...[]wasmkeeper.Option) *Coordinator { + chains := make(map[string]*TestChain) + coord := &Coordinator{ + t: t, + CurrentTime: globalStartTime, + } + + for i := 0; i < n; i++ { + chainID := GetChainID(i) + var x []wasmkeeper.Option + if len(opts) > i { + x = opts[i] + } + chains[chainID] = NewTestChain(t, coord, chainID, x...) + } + coord.Chains = chains + + return coord +} + +// IncrementTime iterates through all the TestChain's and increments their current header time +// by 5 seconds. +// +// CONTRACT: this function must be called after every Commit on any TestChain. +func (coord *Coordinator) IncrementTime() { + coord.IncrementTimeBy(TimeIncrement) +} + +// IncrementTimeBy iterates through all the TestChain's and increments their current header time +// by specified time. +func (coord *Coordinator) IncrementTimeBy(increment time.Duration) { + coord.CurrentTime = coord.CurrentTime.Add(increment).UTC() + coord.UpdateTime() +} + +// UpdateTime updates all clocks for the TestChains to the current global time. +func (coord *Coordinator) UpdateTime() { + for _, chain := range coord.Chains { + coord.UpdateTimeForChain(chain) + } +} + +// UpdateTimeForChain updates the clock for a specific chain. +func (coord *Coordinator) UpdateTimeForChain(chain *TestChain) { + chain.CurrentHeader.Time = coord.CurrentTime.UTC() + chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader}) +} + +// Setup constructs a TM client, connection, and channel on both chains provided. It will +// fail if any error occurs. The clientID's, TestConnections, and TestChannels are returned +// for both chains. The channels created are connected to the ibc-transfer application. +func (coord *Coordinator) Setup(path *Path) { + coord.SetupConnections(path) + + // channels can also be referenced through the returned connections + coord.CreateChannels(path) +} + +// SetupClients is a helper function to create clients on both chains. It assumes the +// caller does not anticipate any errors. +func (coord *Coordinator) SetupClients(path *Path) { + err := path.EndpointA.CreateClient() + require.NoError(coord.t, err) + + err = path.EndpointB.CreateClient() + require.NoError(coord.t, err) +} + +// SetupClientConnections is a helper function to create clients and the appropriate +// connections on both the source and counterparty chain. It assumes the caller does not +// anticipate any errors. +func (coord *Coordinator) SetupConnections(path *Path) { + coord.SetupClients(path) + + coord.CreateConnections(path) +} + +// CreateConnection constructs and executes connection handshake messages in order to create +// OPEN channels on chainA and chainB. The connection information of for chainA and chainB +// are returned within a TestConnection struct. The function expects the connections to be +// successfully opened otherwise testing will fail. +func (coord *Coordinator) CreateConnections(path *Path) { + err := path.EndpointA.ConnOpenInit() + require.NoError(coord.t, err) + + err = path.EndpointB.ConnOpenTry() + require.NoError(coord.t, err) + + err = path.EndpointA.ConnOpenAck() + require.NoError(coord.t, err) + + err = path.EndpointB.ConnOpenConfirm() + require.NoError(coord.t, err) + + // ensure counterparty is up to date + err = path.EndpointA.UpdateClient() + require.NoError(coord.t, err) +} + +// CreateMockChannels constructs and executes channel handshake messages to create OPEN +// channels that use a mock application module that returns nil on all callbacks. This +// function is expects the channels to be successfully opened otherwise testing will +// fail. +func (coord *Coordinator) CreateMockChannels(path *Path) { + path.EndpointA.ChannelConfig.PortID = ibctesting.MockPort + path.EndpointB.ChannelConfig.PortID = ibctesting.MockPort + + coord.CreateChannels(path) +} + +// CreateTransferChannels constructs and executes channel handshake messages to create OPEN +// ibc-transfer channels on chainA and chainB. The function expects the channels to be +// successfully opened otherwise testing will fail. +func (coord *Coordinator) CreateTransferChannels(path *Path) { + path.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + + coord.CreateChannels(path) +} + +// CreateChannel constructs and executes channel handshake messages in order to create +// OPEN channels on chainA and chainB. The function expects the channels to be successfully +// opened otherwise testing will fail. +func (coord *Coordinator) CreateChannels(path *Path) { + err := path.EndpointA.ChanOpenInit() + require.NoError(coord.t, err) + + err = path.EndpointB.ChanOpenTry() + require.NoError(coord.t, err) + + err = path.EndpointA.ChanOpenAck() + require.NoError(coord.t, err) + + err = path.EndpointB.ChanOpenConfirm() + require.NoError(coord.t, err) + + // ensure counterparty is up to date + err = path.EndpointA.UpdateClient() + require.NoError(coord.t, err) +} + +// GetChain returns the TestChain using the given chainID and returns an error if it does +// not exist. +func (coord *Coordinator) GetChain(chainID string) *TestChain { + chain, found := coord.Chains[chainID] + require.True(coord.t, found, fmt.Sprintf("%s chain does not exist", chainID)) + return chain +} + +// GetChainID returns the chainID used for the provided index. +func GetChainID(index int) string { + return ChainIDPrefix + strconv.Itoa(index) +} + +// CommitBlock commits a block on the provided indexes and then increments the global time. +// +// CONTRACT: the passed in list of indexes must not contain duplicates +func (coord *Coordinator) CommitBlock(chains ...*TestChain) { + for _, chain := range chains { + chain.NextBlock() + } + coord.IncrementTime() +} + +// CommitNBlocks commits n blocks to state and updates the block height by 1 for each commit. +func (coord *Coordinator) CommitNBlocks(chain *TestChain, n uint64) { + for i := uint64(0); i < n; i++ { + chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader}) + chain.NextBlock() + coord.IncrementTime() + } +} + +// ConnOpenInitOnBothChains initializes a connection on both endpoints with the state INIT +// using the OpenInit handshake call. +func (coord *Coordinator) ConnOpenInitOnBothChains(path *Path) error { + if err := path.EndpointA.ConnOpenInit(); err != nil { + return err + } + + if err := path.EndpointB.ConnOpenInit(); err != nil { + return err + } + + if err := path.EndpointA.UpdateClient(); err != nil { + return err + } + + if err := path.EndpointB.UpdateClient(); err != nil { + return err + } + + return nil +} + +// ChanOpenInitOnBothChains initializes a channel on the source chain and counterparty chain +// with the state INIT using the OpenInit handshake call. +func (coord *Coordinator) ChanOpenInitOnBothChains(path *Path) error { + // NOTE: only creation of a capability for a transfer or mock port is supported + // Other applications must bind to the port in InitGenesis or modify this code. + + if err := path.EndpointA.ChanOpenInit(); err != nil { + return err + } + + if err := path.EndpointB.ChanOpenInit(); err != nil { + return err + } + + if err := path.EndpointA.UpdateClient(); err != nil { + return err + } + + if err := path.EndpointB.UpdateClient(); err != nil { + return err + } + + return nil +} + +// RelayAndAckPendingPackets sends pending packages from path.EndpointA to the counterparty chain and acks +func (coord *Coordinator) RelayAndAckPendingPackets(path *Path) error { + // get all the packet to relay src->dest + src := path.EndpointA + coord.t.Logf("Relay: %d Packets A->B, %d Packets B->A\n", len(src.Chain.PendingSendPackets), len(path.EndpointB.Chain.PendingSendPackets)) + for i, v := range src.Chain.PendingSendPackets { + err := path.RelayPacket(v, nil) + if err != nil { + return err + } + src.Chain.PendingSendPackets = append(src.Chain.PendingSendPackets[0:i], src.Chain.PendingSendPackets[i+1:]...) + } + + src = path.EndpointB + for i, v := range src.Chain.PendingSendPackets { + err := path.RelayPacket(v, nil) + if err != nil { + return err + } + src.Chain.PendingSendPackets = append(src.Chain.PendingSendPackets[0:i], src.Chain.PendingSendPackets[i+1:]...) + } + return nil +} + +// TimeoutPendingPackets returns the package to source chain to let the IBC app revert any operation. +// from A to A +func (coord *Coordinator) TimeoutPendingPackets(path *Path) error { + src := path.EndpointA + dest := path.EndpointB + + toSend := src.Chain.PendingSendPackets + coord.t.Logf("Timeout %d Packets A->A\n", len(toSend)) + + if err := src.UpdateClient(); err != nil { + return err + } + // Increment time and commit block so that 5 second delay period passes between send and receive + coord.IncrementTime() + coord.CommitBlock(src.Chain, dest.Chain) + for _, packet := range toSend { + // get proof of packet unreceived on dest + packetKey := host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + proofUnreceived, proofHeight := dest.QueryProof(packetKey) + timeoutMsg := channeltypes.NewMsgTimeout(packet, packet.Sequence, proofUnreceived, proofHeight, src.Chain.SenderAccount.GetAddress().String()) + err := src.Chain.sendMsgs(timeoutMsg) + if err != nil { + return err + } + } + src.Chain.PendingSendPackets = nil + return nil +} + +// CloseChannel close channel on both sides +func (coord *Coordinator) CloseChannel(path *Path) { + err := path.EndpointA.ChanCloseInit() + require.NoError(coord.t, err) + coord.IncrementTime() + err = path.EndpointB.UpdateClient() + require.NoError(coord.t, err) + err = path.EndpointB.ChanCloseConfirm() + require.NoError(coord.t, err) +} diff --git a/x/wasm/ibctesting/endpoint.go b/x/wasm/ibctesting/endpoint.go new file mode 100644 index 00000000..e56c5d06 --- /dev/null +++ b/x/wasm/ibctesting/endpoint.go @@ -0,0 +1,597 @@ +package ibctesting + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v4/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + commitmenttypes "github.com/cosmos/ibc-go/v4/modules/core/23-commitment/types" + host "github.com/cosmos/ibc-go/v4/modules/core/24-host" + "github.com/cosmos/ibc-go/v4/modules/core/exported" + ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + "github.com/stretchr/testify/require" +) + +// Endpoint is a which represents a channel endpoint and its associated +// client and connections. It contains client, connection, and channel +// configuration parameters. Endpoint functions will utilize the parameters +// set in the configuration structs when executing IBC messages. +type Endpoint struct { + Chain *TestChain + Counterparty *Endpoint + ClientID string + ConnectionID string + ChannelID string + + ClientConfig ibctesting.ClientConfig + ConnectionConfig *ibctesting.ConnectionConfig + ChannelConfig *ibctesting.ChannelConfig +} + +// NewEndpoint constructs a new endpoint without the counterparty. +// CONTRACT: the counterparty endpoint must be set by the caller. +func NewEndpoint( + chain *TestChain, clientConfig ibctesting.ClientConfig, + connectionConfig *ibctesting.ConnectionConfig, channelConfig *ibctesting.ChannelConfig, +) *Endpoint { + return &Endpoint{ + Chain: chain, + ClientConfig: clientConfig, + ConnectionConfig: connectionConfig, + ChannelConfig: channelConfig, + } +} + +// NewDefaultEndpoint constructs a new endpoint using default values. +// CONTRACT: the counterparty endpoitn must be set by the caller. +func NewDefaultEndpoint(chain *TestChain) *Endpoint { + return &Endpoint{ + Chain: chain, + ClientConfig: ibctesting.NewTendermintConfig(), + ConnectionConfig: ibctesting.NewConnectionConfig(), + ChannelConfig: ibctesting.NewChannelConfig(), + } +} + +// QueryProof queries proof associated with this endpoint using the lastest client state +// height on the counterparty chain. +func (endpoint *Endpoint) QueryProof(key []byte) ([]byte, clienttypes.Height) { + // obtain the counterparty client representing the chain associated with the endpoint + clientState := endpoint.Counterparty.Chain.GetClientState(endpoint.Counterparty.ClientID) + + // query proof on the counterparty using the latest height of the IBC client + return endpoint.QueryProofAtHeight(key, clientState.GetLatestHeight().GetRevisionHeight()) +} + +// QueryProofAtHeight queries proof associated with this endpoint using the proof height +// provided +func (endpoint *Endpoint) QueryProofAtHeight(key []byte, height uint64) ([]byte, clienttypes.Height) { + // query proof on the counterparty using the latest height of the IBC client + return endpoint.Chain.QueryProofAtHeight(key, int64(height)) +} + +// CreateClient creates an IBC client on the endpoint. It will update the +// clientID for the endpoint if the message is successfully executed. +// NOTE: a solo machine client will be created with an empty diversifier. +func (endpoint *Endpoint) CreateClient() (err error) { + // ensure counterparty has committed state + endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain) + + var ( + clientState exported.ClientState + consensusState exported.ConsensusState + ) + + switch endpoint.ClientConfig.GetClientType() { + case exported.Tendermint: + tmConfig, ok := endpoint.ClientConfig.(*ibctesting.TendermintConfig) + require.True(endpoint.Chain.t, ok) + + height := endpoint.Counterparty.Chain.LastHeader.GetHeight().(clienttypes.Height) + clientState = ibctmtypes.NewClientState( + endpoint.Counterparty.Chain.ChainID, tmConfig.TrustLevel, tmConfig.TrustingPeriod, tmConfig.UnbondingPeriod, tmConfig.MaxClockDrift, + height, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath, tmConfig.AllowUpdateAfterExpiry, tmConfig.AllowUpdateAfterMisbehaviour, + ) + consensusState = endpoint.Counterparty.Chain.LastHeader.ConsensusState() + case exported.Solomachine: + // TODO + // solo := NewSolomachine(chain.t, endpoint.Chain.Codec, clientID, "", 1) + // clientState = solo.ClientState() + // consensusState = solo.ConsensusState() + + default: + err = fmt.Errorf("client type %s is not supported", endpoint.ClientConfig.GetClientType()) + } + + if err != nil { + return err + } + + msg, err := clienttypes.NewMsgCreateClient( + clientState, consensusState, endpoint.Chain.SenderAccount.GetAddress().String(), + ) + require.NoError(endpoint.Chain.t, err) + + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + endpoint.ClientID, err = ibctesting.ParseClientIDFromEvents(res.GetEvents()) + require.NoError(endpoint.Chain.t, err) + + return nil +} + +// UpdateClient updates the IBC client associated with the endpoint. +func (endpoint *Endpoint) UpdateClient() (err error) { + // ensure counterparty has committed state + endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain) + + var header exported.Header + + switch endpoint.ClientConfig.GetClientType() { + case exported.Tendermint: + header, err = endpoint.Chain.ConstructUpdateTMClientHeader(endpoint.Counterparty.Chain, endpoint.ClientID) + + default: + err = fmt.Errorf("client type %s is not supported", endpoint.ClientConfig.GetClientType()) + } + + if err != nil { + return err + } + + msg, err := clienttypes.NewMsgUpdateClient( + endpoint.ClientID, header, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + require.NoError(endpoint.Chain.t, err) + + return endpoint.Chain.sendMsgs(msg) +} + +// ConnOpenInit will construct and execute a MsgConnectionOpenInit on the associated endpoint. +func (endpoint *Endpoint) ConnOpenInit() error { + msg := connectiontypes.NewMsgConnectionOpenInit( + endpoint.ClientID, + endpoint.Counterparty.ClientID, + endpoint.Counterparty.Chain.GetPrefix(), ibctesting.DefaultOpenInitVersion, endpoint.ConnectionConfig.DelayPeriod, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + endpoint.ConnectionID, err = ibctesting.ParseConnectionIDFromEvents(res.GetEvents()) + require.NoError(endpoint.Chain.t, err) + + return nil +} + +// ConnOpenTry will construct and execute a MsgConnectionOpenTry on the associated endpoint. +func (endpoint *Endpoint) ConnOpenTry() error { + if err := endpoint.UpdateClient(); err != nil { + return err + } + + counterpartyClient, proofClient, proofConsensus, consensusHeight, proofInit, proofHeight := endpoint.QueryConnectionHandshakeProof() + + msg := connectiontypes.NewMsgConnectionOpenTry( + endpoint.ClientID, endpoint.Counterparty.ConnectionID, endpoint.Counterparty.ClientID, + counterpartyClient, endpoint.Counterparty.Chain.GetPrefix(), []*connectiontypes.Version{ibctesting.ConnectionVersion}, endpoint.ConnectionConfig.DelayPeriod, + proofInit, proofClient, proofConsensus, + proofHeight, consensusHeight, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + if endpoint.ConnectionID == "" { + endpoint.ConnectionID, err = ibctesting.ParseConnectionIDFromEvents(res.GetEvents()) + require.NoError(endpoint.Chain.t, err) + } + + return nil +} + +// ConnOpenAck will construct and execute a MsgConnectionOpenAck on the associated endpoint. +func (endpoint *Endpoint) ConnOpenAck() error { + if err := endpoint.UpdateClient(); err != nil { + return err + } + + counterpartyClient, proofClient, proofConsensus, consensusHeight, proofTry, proofHeight := endpoint.QueryConnectionHandshakeProof() + + msg := connectiontypes.NewMsgConnectionOpenAck( + endpoint.ConnectionID, endpoint.Counterparty.ConnectionID, counterpartyClient, // testing doesn't use flexible selection + proofTry, proofClient, proofConsensus, + proofHeight, consensusHeight, + ibctesting.ConnectionVersion, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + return endpoint.Chain.sendMsgs(msg) +} + +// ConnOpenConfirm will construct and execute a MsgConnectionOpenConfirm on the associated endpoint. +func (endpoint *Endpoint) ConnOpenConfirm() error { + if err := endpoint.UpdateClient(); err != nil { + return err + } + + connectionKey := host.ConnectionKey(endpoint.Counterparty.ConnectionID) + proof, height := endpoint.Counterparty.Chain.QueryProof(connectionKey) + + msg := connectiontypes.NewMsgConnectionOpenConfirm( + endpoint.ConnectionID, + proof, height, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + return endpoint.Chain.sendMsgs(msg) +} + +// QueryConnectionHandshakeProof returns all the proofs necessary to execute OpenTry or Open Ack of +// the connection handshakes. It returns the counterparty client state, proof of the counterparty +// client state, proof of the counterparty consensus state, the consensus state height, proof of +// the counterparty connection, and the proof height for all the proofs returned. +func (endpoint *Endpoint) QueryConnectionHandshakeProof() ( + clientState exported.ClientState, proofClient, + proofConsensus []byte, consensusHeight clienttypes.Height, + proofConnection []byte, proofHeight clienttypes.Height, +) { + // obtain the client state on the counterparty chain + clientState = endpoint.Counterparty.Chain.GetClientState(endpoint.Counterparty.ClientID) + + // query proof for the client state on the counterparty + clientKey := host.FullClientStateKey(endpoint.Counterparty.ClientID) + proofClient, proofHeight = endpoint.Counterparty.QueryProof(clientKey) + + consensusHeight = clientState.GetLatestHeight().(clienttypes.Height) + + // query proof for the consensus state on the counterparty + consensusKey := host.FullConsensusStateKey(endpoint.Counterparty.ClientID, consensusHeight) + proofConsensus, _ = endpoint.Counterparty.QueryProofAtHeight(consensusKey, proofHeight.GetRevisionHeight()) + + // query proof for the connection on the counterparty + connectionKey := host.ConnectionKey(endpoint.Counterparty.ConnectionID) + proofConnection, _ = endpoint.Counterparty.QueryProofAtHeight(connectionKey, proofHeight.GetRevisionHeight()) + + return +} + +// ChanOpenInit will construct and execute a MsgChannelOpenInit on the associated endpoint. +func (endpoint *Endpoint) ChanOpenInit() error { + msg := channeltypes.NewMsgChannelOpenInit( + endpoint.ChannelConfig.PortID, + endpoint.ChannelConfig.Version, endpoint.ChannelConfig.Order, []string{endpoint.ConnectionID}, + endpoint.Counterparty.ChannelConfig.PortID, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + endpoint.ChannelID, err = ibctesting.ParseChannelIDFromEvents(res.GetEvents()) + require.NoError(endpoint.Chain.t, err) + + // update version to selected app version + // NOTE: this update must be performed after SendMsgs() + endpoint.ChannelConfig.Version = endpoint.GetChannel().Version + + return nil +} + +// ChanOpenTry will construct and execute a MsgChannelOpenTry on the associated endpoint. +func (endpoint *Endpoint) ChanOpenTry() error { + if err := endpoint.UpdateClient(); err != nil { + return err + } + + channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID) + proof, height := endpoint.Counterparty.Chain.QueryProof(channelKey) + + msg := channeltypes.NewMsgChannelOpenTry( + endpoint.ChannelConfig.PortID, + endpoint.ChannelConfig.Version, endpoint.ChannelConfig.Order, []string{endpoint.ConnectionID}, + endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID, endpoint.Counterparty.ChannelConfig.Version, + proof, height, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + if endpoint.ChannelID == "" { + endpoint.ChannelID, err = ibctesting.ParseChannelIDFromEvents(res.GetEvents()) + require.NoError(endpoint.Chain.t, err) + } + + // update version to selected app version + // NOTE: this update must be performed after the endpoint channelID is set + endpoint.ChannelConfig.Version = endpoint.GetChannel().Version + + return nil +} + +// ChanOpenAck will construct and execute a MsgChannelOpenAck on the associated endpoint. +func (endpoint *Endpoint) ChanOpenAck() error { + if err := endpoint.UpdateClient(); err != nil { + return err + } + + channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID) + proof, height := endpoint.Counterparty.Chain.QueryProof(channelKey) + + msg := channeltypes.NewMsgChannelOpenAck( + endpoint.ChannelConfig.PortID, endpoint.ChannelID, + endpoint.Counterparty.ChannelID, endpoint.Counterparty.ChannelConfig.Version, // testing doesn't use flexible selection + proof, height, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + if err := endpoint.Chain.sendMsgs(msg); err != nil { + return err + } + + endpoint.ChannelConfig.Version = endpoint.GetChannel().Version + return nil +} + +// ChanOpenConfirm will construct and execute a MsgChannelOpenConfirm on the associated endpoint. +func (endpoint *Endpoint) ChanOpenConfirm() error { + if err := endpoint.UpdateClient(); err != nil { + return err + } + + channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID) + proof, height := endpoint.Counterparty.Chain.QueryProof(channelKey) + + msg := channeltypes.NewMsgChannelOpenConfirm( + endpoint.ChannelConfig.PortID, endpoint.ChannelID, + proof, height, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + return endpoint.Chain.sendMsgs(msg) +} + +// ChanCloseInit will construct and execute a MsgChannelCloseInit on the associated endpoint. +// +// NOTE: does not work with ibc-transfer module +func (endpoint *Endpoint) ChanCloseInit() error { + msg := channeltypes.NewMsgChannelCloseInit( + endpoint.ChannelConfig.PortID, endpoint.ChannelID, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + return endpoint.Chain.sendMsgs(msg) +} + +// ChanCloseConfirm will construct and execute a NewMsgChannelCloseConfirm on the associated endpoint. +func (endpoint *Endpoint) ChanCloseConfirm() error { + channelKey := host.ChannelKey(endpoint.Counterparty.ChannelConfig.PortID, endpoint.Counterparty.ChannelID) + proof, proofHeight := endpoint.Counterparty.QueryProof(channelKey) + + msg := channeltypes.NewMsgChannelCloseConfirm( + endpoint.ChannelConfig.PortID, endpoint.ChannelID, + proof, proofHeight, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + return endpoint.Chain.sendMsgs(msg) +} + +// SendPacket sends a packet through the channel keeper using the associated endpoint +// The counterparty client is updated so proofs can be sent to the counterparty chain. +func (endpoint *Endpoint) SendPacket(packet exported.PacketI) error { + channelCap := endpoint.Chain.GetChannelCapability(packet.GetSourcePort(), packet.GetSourceChannel()) + + // no need to send message, acting as a module + err := endpoint.Chain.App.IBCKeeper.ChannelKeeper.SendPacket(endpoint.Chain.GetContext(), channelCap, packet) + if err != nil { + return err + } + + // commit changes since no message was sent + endpoint.Chain.Coordinator.CommitBlock(endpoint.Chain) + + return endpoint.Counterparty.UpdateClient() +} + +// RecvPacket receives a packet on the associated endpoint. +// The counterparty client is updated. +func (endpoint *Endpoint) RecvPacket(packet channeltypes.Packet) error { + _, err := endpoint.RecvPacketWithResult(packet) + if err != nil { + return err + } + + return nil +} + +// RecvPacketWithResult receives a packet on the associated endpoint and the result +// of the transaction is returned. The counterparty client is updated. +func (endpoint *Endpoint) RecvPacketWithResult(packet channeltypes.Packet) (*sdk.Result, error) { + // get proof of packet commitment on source + packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + proof, proofHeight := endpoint.Counterparty.Chain.QueryProof(packetKey) + + recvMsg := channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String()) + + // receive on counterparty and update source client + res, err := endpoint.Chain.SendMsgs(recvMsg) + if err != nil { + return nil, err + } + + if err := endpoint.Counterparty.UpdateClient(); err != nil { + return nil, err + } + + return res, nil +} + +// WriteAcknowledgement writes an acknowledgement on the channel associated with the endpoint. +// The counterparty client is updated. +func (endpoint *Endpoint) WriteAcknowledgement(ack exported.Acknowledgement, packet exported.PacketI) error { + channelCap := endpoint.Chain.GetChannelCapability(packet.GetDestPort(), packet.GetDestChannel()) + + // no need to send message, acting as a handler + err := endpoint.Chain.App.IBCKeeper.ChannelKeeper.WriteAcknowledgement(endpoint.Chain.GetContext(), channelCap, packet, ack) + if err != nil { + return err + } + + // commit changes since no message was sent + endpoint.Chain.Coordinator.CommitBlock(endpoint.Chain) + + return endpoint.Counterparty.UpdateClient() +} + +// AcknowledgePacket sends a MsgAcknowledgement to the channel associated with the endpoint. +func (endpoint *Endpoint) AcknowledgePacket(packet channeltypes.Packet, ack []byte) error { + // get proof of acknowledgement on counterparty + packetKey := host.PacketAcknowledgementKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + proof, proofHeight := endpoint.Counterparty.QueryProof(packetKey) + + ackMsg := channeltypes.NewMsgAcknowledgement(packet, ack, proof, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String()) + + return endpoint.Chain.sendMsgs(ackMsg) +} + +// TimeoutPacket sends a MsgTimeout to the channel associated with the endpoint. +func (endpoint *Endpoint) TimeoutPacket(packet channeltypes.Packet) error { + // get proof for timeout based on channel order + var packetKey []byte + + switch endpoint.ChannelConfig.Order { + case channeltypes.ORDERED: + packetKey = host.NextSequenceRecvKey(packet.GetDestPort(), packet.GetDestChannel()) + case channeltypes.UNORDERED: + packetKey = host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + default: + return fmt.Errorf("unsupported order type %s", endpoint.ChannelConfig.Order) + } + + proof, proofHeight := endpoint.Counterparty.QueryProof(packetKey) + nextSeqRecv, found := endpoint.Counterparty.Chain.App.IBCKeeper.ChannelKeeper.GetNextSequenceRecv(endpoint.Counterparty.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID) + require.True(endpoint.Chain.t, found) + + timeoutMsg := channeltypes.NewMsgTimeout( + packet, nextSeqRecv, + proof, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String(), + ) + + return endpoint.Chain.sendMsgs(timeoutMsg) +} + +// TimeoutOnClose sends a MsgTimeoutOnClose to the channel associated with the endpoint. +func (endpoint *Endpoint) TimeoutOnClose(packet channeltypes.Packet) error { + // get proof for timeout based on channel order + var packetKey []byte + + switch endpoint.ChannelConfig.Order { + case channeltypes.ORDERED: + packetKey = host.NextSequenceRecvKey(packet.GetDestPort(), packet.GetDestChannel()) + case channeltypes.UNORDERED: + packetKey = host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + default: + return fmt.Errorf("unsupported order type %s", endpoint.ChannelConfig.Order) + } + + proof, proofHeight := endpoint.Counterparty.QueryProof(packetKey) + + channelKey := host.ChannelKey(packet.GetDestPort(), packet.GetDestChannel()) + proofClosed, _ := endpoint.Counterparty.QueryProof(channelKey) + + nextSeqRecv, found := endpoint.Counterparty.Chain.App.IBCKeeper.ChannelKeeper.GetNextSequenceRecv(endpoint.Counterparty.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID) + require.True(endpoint.Chain.t, found) + + timeoutOnCloseMsg := channeltypes.NewMsgTimeoutOnClose( + packet, nextSeqRecv, + proof, proofClosed, proofHeight, endpoint.Chain.SenderAccount.GetAddress().String(), + ) + + return endpoint.Chain.sendMsgs(timeoutOnCloseMsg) +} + +// SetChannelClosed sets a channel state to CLOSED. +func (endpoint *Endpoint) SetChannelClosed() error { + channel := endpoint.GetChannel() + + channel.State = channeltypes.CLOSED + endpoint.Chain.App.IBCKeeper.ChannelKeeper.SetChannel(endpoint.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID, channel) + + endpoint.Chain.Coordinator.CommitBlock(endpoint.Chain) + + return endpoint.Counterparty.UpdateClient() +} + +// GetClientState retrieves the Client State for this endpoint. The +// client state is expected to exist otherwise testing will fail. +func (endpoint *Endpoint) GetClientState() exported.ClientState { + return endpoint.Chain.GetClientState(endpoint.ClientID) +} + +// SetClientState sets the client state for this endpoint. +func (endpoint *Endpoint) SetClientState(clientState exported.ClientState) { + endpoint.Chain.App.IBCKeeper.ClientKeeper.SetClientState(endpoint.Chain.GetContext(), endpoint.ClientID, clientState) +} + +// GetConsensusState retrieves the Consensus State for this endpoint at the provided height. +// The consensus state is expected to exist otherwise testing will fail. +func (endpoint *Endpoint) GetConsensusState(height exported.Height) exported.ConsensusState { + consensusState, found := endpoint.Chain.GetConsensusState(endpoint.ClientID, height) + require.True(endpoint.Chain.t, found) + + return consensusState +} + +// SetConsensusState sets the consensus state for this endpoint. +func (endpoint *Endpoint) SetConsensusState(consensusState exported.ConsensusState, height exported.Height) { + endpoint.Chain.App.IBCKeeper.ClientKeeper.SetClientConsensusState(endpoint.Chain.GetContext(), endpoint.ClientID, height, consensusState) +} + +// GetConnection retrieves an IBC Connection for the endpoint. The +// connection is expected to exist otherwise testing will fail. +func (endpoint *Endpoint) GetConnection() connectiontypes.ConnectionEnd { + connection, found := endpoint.Chain.App.IBCKeeper.ConnectionKeeper.GetConnection(endpoint.Chain.GetContext(), endpoint.ConnectionID) + require.True(endpoint.Chain.t, found) + + return connection +} + +// SetConnection sets the connection for this endpoint. +func (endpoint *Endpoint) SetConnection(connection connectiontypes.ConnectionEnd) { + endpoint.Chain.App.IBCKeeper.ConnectionKeeper.SetConnection(endpoint.Chain.GetContext(), endpoint.ConnectionID, connection) +} + +// GetChannel retrieves an IBC Channel for the endpoint. The channel +// is expected to exist otherwise testing will fail. +func (endpoint *Endpoint) GetChannel() channeltypes.Channel { + channel, found := endpoint.Chain.App.IBCKeeper.ChannelKeeper.GetChannel(endpoint.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID) + require.True(endpoint.Chain.t, found) + + return channel +} + +// SetChannel sets the channel for this endpoint. +func (endpoint *Endpoint) SetChannel(channel channeltypes.Channel) { + endpoint.Chain.App.IBCKeeper.ChannelKeeper.SetChannel(endpoint.Chain.GetContext(), endpoint.ChannelConfig.PortID, endpoint.ChannelID, channel) +} + +// QueryClientStateProof performs and abci query for a client stat associated +// with this endpoint and returns the ClientState along with the proof. +func (endpoint *Endpoint) QueryClientStateProof() (exported.ClientState, []byte) { + // retrieve client state to provide proof for + clientState := endpoint.GetClientState() + + clientKey := host.FullClientStateKey(endpoint.ClientID) + proofClient, _ := endpoint.QueryProof(clientKey) + + return clientState, proofClient +} diff --git a/x/wasm/ibctesting/event_utils.go b/x/wasm/ibctesting/event_utils.go new file mode 100644 index 00000000..0933dadd --- /dev/null +++ b/x/wasm/ibctesting/event_utils.go @@ -0,0 +1,118 @@ +package ibctesting + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +func getSendPackets(evts []abci.Event) []channeltypes.Packet { + var res []channeltypes.Packet + for _, evt := range evts { + if evt.Type == channeltypes.EventTypeSendPacket { + packet := parsePacketFromEvent(evt) + res = append(res, packet) + } + } + return res +} + +// Used for various debug statements above when needed... do not remove +// func showEvent(evt abci.Event) { +// fmt.Printf("evt.Type: %s\n", evt.Type) +// for _, attr := range evt.Attributes { +// fmt.Printf(" %s = %s\n", string(attr.Key), string(attr.Value)) +// } +//} + +func parsePacketFromEvent(evt abci.Event) channeltypes.Packet { + return channeltypes.Packet{ + Sequence: getUintField(evt, channeltypes.AttributeKeySequence), + SourcePort: getField(evt, channeltypes.AttributeKeySrcPort), + SourceChannel: getField(evt, channeltypes.AttributeKeySrcChannel), + DestinationPort: getField(evt, channeltypes.AttributeKeyDstPort), + DestinationChannel: getField(evt, channeltypes.AttributeKeyDstChannel), + Data: getHexField(evt, channeltypes.AttributeKeyDataHex), + TimeoutHeight: parseTimeoutHeight(getField(evt, channeltypes.AttributeKeyTimeoutHeight)), + TimeoutTimestamp: getUintField(evt, channeltypes.AttributeKeyTimeoutTimestamp), + } +} + +func getHexField(evt abci.Event, key string) []byte { + got := getField(evt, key) + if got == "" { + return nil + } + bz, err := hex.DecodeString(got) + if err != nil { + panic(err) + } + return bz +} + +// return the value for the attribute with the given name +func getField(evt abci.Event, key string) string { + for _, attr := range evt.Attributes { + if string(attr.Key) == key { + return string(attr.Value) + } + } + return "" +} + +func getUintField(evt abci.Event, key string) uint64 { + raw := getField(evt, key) + return toUint64(raw) +} + +func toUint64(raw string) uint64 { + if raw == "" { + return 0 + } + i, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + panic(err) + } + return i +} + +func parseTimeoutHeight(raw string) clienttypes.Height { + chunks := strings.Split(raw, "-") + return clienttypes.Height{ + RevisionNumber: toUint64(chunks[0]), + RevisionHeight: toUint64(chunks[1]), + } +} + +func ParsePortIDFromEvents(events sdk.Events) (string, error) { + for _, ev := range events { + if ev.Type == channeltypes.EventTypeChannelOpenInit || ev.Type == channeltypes.EventTypeChannelOpenTry { + for _, attr := range ev.Attributes { + if string(attr.Key) == channeltypes.AttributeKeyPortID { + return string(attr.Value), nil + } + } + } + } + return "", fmt.Errorf("port id event attribute not found") +} + +func ParseChannelVersionFromEvents(events sdk.Events) (string, error) { + for _, ev := range events { + if ev.Type == channeltypes.EventTypeChannelOpenInit || ev.Type == channeltypes.EventTypeChannelOpenTry { + for _, attr := range ev.Attributes { + if string(attr.Key) == channeltypes.AttributeVersion { + return string(attr.Value), nil + } + } + } + } + return "", fmt.Errorf("version event attribute not found") +} diff --git a/x/wasm/ibctesting/faucet.go b/x/wasm/ibctesting/faucet.go new file mode 100644 index 00000000..d72d4437 --- /dev/null +++ b/x/wasm/ibctesting/faucet.go @@ -0,0 +1,52 @@ +package ibctesting + +import ( + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/app" +) + +// Fund an address with the given amount in default denom +func (chain *TestChain) Fund(addr sdk.AccAddress, amount sdk.Int) { + require.NoError(chain.t, chain.sendMsgs(&banktypes.MsgSend{ + FromAddress: chain.SenderAccount.GetAddress().String(), + ToAddress: addr.String(), + Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)), + })) +} + +// SendNonDefaultSenderMsgs delivers a transaction through the application. It returns the result and error if one +// occurred. +func (chain *TestChain) SendNonDefaultSenderMsgs(senderPrivKey cryptotypes.PrivKey, msgs ...sdk.Msg) (*sdk.Result, error) { + require.NotEqual(chain.t, chain.SenderPrivKey, senderPrivKey, "use SendMsgs method") + + // ensure the chain has the latest time + chain.Coordinator.UpdateTimeForChain(chain) + + addr := sdk.AccAddress(senderPrivKey.PubKey().Address().Bytes()) + account := chain.App.AccountKeeper.GetAccount(chain.GetContext(), addr) + require.NotNil(chain.t, account) + _, r, err := app.SignAndDeliver( + chain.t, + chain.TxConfig, + chain.App.BaseApp, + chain.GetContext().BlockHeader(), + msgs, + chain.ChainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + senderPrivKey, + ) + + // SignAndDeliver calls app.Commit() + chain.NextBlock() + chain.Coordinator.IncrementTime() + if err != nil { + return r, err + } + chain.captureIBCEvents(r) + return r, nil +} diff --git a/x/wasm/ibctesting/path.go b/x/wasm/ibctesting/path.go new file mode 100644 index 00000000..5e861325 --- /dev/null +++ b/x/wasm/ibctesting/path.go @@ -0,0 +1,113 @@ +package ibctesting + +import ( + "bytes" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" +) + +// Path contains two endpoints representing two chains connected over IBC +type Path struct { + EndpointA *Endpoint + EndpointB *Endpoint +} + +// NewPath constructs an endpoint for each chain using the default values +// for the endpoints. Each endpoint is updated to have a pointer to the +// counterparty endpoint. +func NewPath(chainA, chainB *TestChain) *Path { + endpointA := NewDefaultEndpoint(chainA) + endpointB := NewDefaultEndpoint(chainB) + + endpointA.Counterparty = endpointB + endpointB.Counterparty = endpointA + + return &Path{ + EndpointA: endpointA, + EndpointB: endpointB, + } +} + +// SetChannelOrdered sets the channel order for both endpoints to ORDERED. +func (path *Path) SetChannelOrdered() { + path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED + path.EndpointB.ChannelConfig.Order = channeltypes.ORDERED +} + +// RelayPacket attempts to relay the packet first on EndpointA and then on EndpointB +// if EndpointA does not contain a packet commitment for that packet. An error is returned +// if a relay step fails or the packet commitment does not exist on either endpoint. +func (path *Path) RelayPacket(packet channeltypes.Packet, ack []byte) error { + pc := path.EndpointA.Chain.App.IBCKeeper.ChannelKeeper.GetPacketCommitment(path.EndpointA.Chain.GetContext(), packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + if bytes.Equal(pc, channeltypes.CommitPacket(path.EndpointA.Chain.App.AppCodec(), packet)) { + + // packet found, relay from A to B + if err := path.EndpointB.UpdateClient(); err != nil { + return err + } + + res, err := path.EndpointB.RecvPacketWithResult(packet) + if err != nil { + return err + } + + ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) + if err != nil { + return err + } + + if err := path.EndpointA.AcknowledgePacket(packet, ack); err != nil { + return err + } + + return nil + } + + pc = path.EndpointB.Chain.App.IBCKeeper.ChannelKeeper.GetPacketCommitment(path.EndpointB.Chain.GetContext(), packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + if bytes.Equal(pc, channeltypes.CommitPacket(path.EndpointB.Chain.App.AppCodec(), packet)) { + + // packet found, relay B to A + if err := path.EndpointA.UpdateClient(); err != nil { + return err + } + + res, err := path.EndpointA.RecvPacketWithResult(packet) + if err != nil { + return err + } + + ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) + if err != nil { + return err + } + + if err := path.EndpointB.AcknowledgePacket(packet, ack); err != nil { + return err + } + return nil + } + + return fmt.Errorf("packet commitment does not exist on either endpoint for provided packet") +} + +// SendMsg delivers the provided messages to the chain. The counterparty +// client is updated with the new source consensus state. +func (path *Path) SendMsg(msgs ...sdk.Msg) error { + if err := path.EndpointA.Chain.sendMsgs(msgs...); err != nil { + return err + } + if err := path.EndpointA.UpdateClient(); err != nil { + return err + } + return path.EndpointB.UpdateClient() +} + +func (path *Path) Invert() *Path { + return &Path{ + EndpointA: path.EndpointB, + EndpointB: path.EndpointA, + } +} diff --git a/x/wasm/ibctesting/wasm.go b/x/wasm/ibctesting/wasm.go new file mode 100644 index 00000000..80eb87e2 --- /dev/null +++ b/x/wasm/ibctesting/wasm.go @@ -0,0 +1,138 @@ +package ibctesting + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "os" + "strings" + + ibctesting "github.com/cosmos/ibc-go/v4/testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/protobuf/proto" //nolint + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/rand" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var wasmIdent = []byte("\x00\x61\x73\x6D") + +// SeedNewContractInstance stores some wasm code and instantiates a new contract on this chain. +// This method can be called to prepare the store with some valid CodeInfo and ContractInfo. The returned +// Address is the contract address for this instance. Test should make use of this data and/or use NewIBCContractMockWasmer +// for using a contract mock in Go. +func (chain *TestChain) SeedNewContractInstance() sdk.AccAddress { + pInstResp := chain.StoreCode(append(wasmIdent, rand.Bytes(10)...)) + codeID := pInstResp.CodeID + + anyAddressStr := chain.SenderAccount.GetAddress().String() + initMsg := []byte(fmt.Sprintf(`{"verifier": %q, "beneficiary": %q}`, anyAddressStr, anyAddressStr)) + return chain.InstantiateContract(codeID, initMsg) +} + +func (chain *TestChain) StoreCodeFile(filename string) types.MsgStoreCodeResponse { + wasmCode, err := os.ReadFile(filename) + require.NoError(chain.t, err) + if strings.HasSuffix(filename, "wasm") { // compress for gas limit + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + _, err := gz.Write(wasmCode) + require.NoError(chain.t, err) + err = gz.Close() + require.NoError(chain.t, err) + wasmCode = buf.Bytes() + } + return chain.StoreCode(wasmCode) +} + +func (chain *TestChain) StoreCode(byteCode []byte) types.MsgStoreCodeResponse { + storeMsg := &types.MsgStoreCode{ + Sender: chain.SenderAccount.GetAddress().String(), + WASMByteCode: byteCode, + } + r, err := chain.SendMsgs(storeMsg) + require.NoError(chain.t, err) + protoResult := chain.parseSDKResultData(r) + require.Len(chain.t, protoResult.Data, 1) + // unmarshal protobuf response from data + var pInstResp types.MsgStoreCodeResponse + require.NoError(chain.t, pInstResp.Unmarshal(protoResult.Data[0].Data)) + require.NotEmpty(chain.t, pInstResp.CodeID) + require.NotEmpty(chain.t, pInstResp.Checksum) + return pInstResp +} + +func (chain *TestChain) InstantiateContract(codeID uint64, initMsg []byte) sdk.AccAddress { + instantiateMsg := &types.MsgInstantiateContract{ + Sender: chain.SenderAccount.GetAddress().String(), + Admin: chain.SenderAccount.GetAddress().String(), + CodeID: codeID, + Label: "ibc-test", + Msg: initMsg, + Funds: sdk.Coins{ibctesting.TestCoin}, + } + + r, err := chain.SendMsgs(instantiateMsg) + require.NoError(chain.t, err) + protoResult := chain.parseSDKResultData(r) + require.Len(chain.t, protoResult.Data, 1) + + var pExecResp types.MsgInstantiateContractResponse + require.NoError(chain.t, pExecResp.Unmarshal(protoResult.Data[0].Data)) + a, err := sdk.AccAddressFromBech32(pExecResp.Address) + require.NoError(chain.t, err) + return a +} + +// SmartQuery This will serialize the query message and submit it to the contract. +// The response is parsed into the provided interface. +// Usage: SmartQuery(addr, QueryMsg{Foo: 1}, &response) +func (chain *TestChain) SmartQuery(contractAddr string, queryMsg interface{}, response interface{}) error { + msg, err := json.Marshal(queryMsg) + if err != nil { + return err + } + + req := types.QuerySmartContractStateRequest{ + Address: contractAddr, + QueryData: msg, + } + reqBin, err := proto.Marshal(&req) + if err != nil { + return err + } + + // TODO: what is the query? + res := chain.App.Query(abci.RequestQuery{ + Path: "/cosmwasm.wasm.v1.Query/SmartContractState", + Data: reqBin, + }) + + if res.Code != 0 { + return fmt.Errorf("query failed: (%d) %s", res.Code, res.Log) + } + + // unpack protobuf + var resp types.QuerySmartContractStateResponse + err = proto.Unmarshal(res.Value, &resp) + if err != nil { + return err + } + // unpack json content + return json.Unmarshal(resp.Data, response) +} + +func (chain *TestChain) parseSDKResultData(r *sdk.Result) sdk.TxMsgData { + var protoResult sdk.TxMsgData + require.NoError(chain.t, proto.Unmarshal(r.Data, &protoResult)) + return protoResult +} + +// ContractInfo is a helper function to returns the ContractInfo for the given contract address +func (chain *TestChain) ContractInfo(contractAddr sdk.AccAddress) *types.ContractInfo { + return chain.App.WasmKeeper.GetContractInfo(chain.GetContext(), contractAddr) +} diff --git a/x/wasm/ioutils/ioutil.go b/x/wasm/ioutils/ioutil.go new file mode 100644 index 00000000..890f3ab2 --- /dev/null +++ b/x/wasm/ioutils/ioutil.go @@ -0,0 +1,41 @@ +package ioutils + +import ( + "bytes" + "compress/gzip" + "io" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// Uncompress expects a valid gzip source to unpack or fails. See IsGzip +func Uncompress(gzipSrc []byte, limit uint64) ([]byte, error) { + if uint64(len(gzipSrc)) > limit { + return nil, types.ErrLimit + } + zr, err := gzip.NewReader(bytes.NewReader(gzipSrc)) + if err != nil { + return nil, err + } + zr.Multistream(false) + defer zr.Close() + return io.ReadAll(LimitReader(zr, int64(limit))) +} + +// LimitReader returns a Reader that reads from r +// but stops with types.ErrLimit after n bytes. +// The underlying implementation is a *io.LimitedReader. +func LimitReader(r io.Reader, n int64) io.Reader { + return &LimitedReader{r: &io.LimitedReader{R: r, N: n}} +} + +type LimitedReader struct { + r *io.LimitedReader +} + +func (l *LimitedReader) Read(p []byte) (n int, err error) { + if l.r.N <= 0 { + return 0, types.ErrLimit + } + return l.r.Read(p) +} diff --git a/x/wasm/ioutils/ioutil_test.go b/x/wasm/ioutils/ioutil_test.go new file mode 100644 index 00000000..c57ecaa5 --- /dev/null +++ b/x/wasm/ioutils/ioutil_test.go @@ -0,0 +1,82 @@ +package ioutils + +import ( + "bytes" + "compress/gzip" + "errors" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestUncompress(t *testing.T) { + wasmRaw, err := os.ReadFile("../keeper/testdata/hackatom.wasm") + require.NoError(t, err) + + wasmGzipped, err := os.ReadFile("../keeper/testdata/hackatom.wasm.gzip") + require.NoError(t, err) + + const maxSize = 400_000 + + specs := map[string]struct { + src []byte + expError error + expResult []byte + }{ + "handle wasm compressed": { + src: wasmGzipped, + expResult: wasmRaw, + }, + "handle gzip identifier only": { + src: gzipIdent, + expError: io.ErrUnexpectedEOF, + }, + "handle broken gzip": { + src: append(gzipIdent, byte(0x1)), + expError: io.ErrUnexpectedEOF, + }, + "handle incomplete gzip": { + src: wasmGzipped[:len(wasmGzipped)-5], + expError: io.ErrUnexpectedEOF, + }, + "handle limit gzip output": { + src: asGzip(bytes.Repeat([]byte{0x1}, maxSize)), + expResult: bytes.Repeat([]byte{0x1}, maxSize), + }, + "handle big gzip output": { + src: asGzip(bytes.Repeat([]byte{0x1}, maxSize+1)), + expError: types.ErrLimit, + }, + "handle other big gzip output": { + src: asGzip(bytes.Repeat([]byte{0x1}, 2*maxSize)), + expError: types.ErrLimit, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + r, err := Uncompress(spec.src, maxSize) + require.True(t, errors.Is(spec.expError, err), "exp %v got %+v", spec.expError, err) + if spec.expError != nil { + return + } + assert.Equal(t, spec.expResult, r) + }) + } +} + +func asGzip(src []byte) []byte { + var buf bytes.Buffer + zipper := gzip.NewWriter(&buf) + if _, err := io.Copy(zipper, bytes.NewReader(src)); err != nil { + panic(err) + } + if err := zipper.Close(); err != nil { + panic(err) + } + return buf.Bytes() +} diff --git a/x/wasm/ioutils/utils.go b/x/wasm/ioutils/utils.go new file mode 100644 index 00000000..197c44c3 --- /dev/null +++ b/x/wasm/ioutils/utils.go @@ -0,0 +1,43 @@ +package ioutils + +import ( + "bytes" + "compress/gzip" +) + +// Note: []byte can never be const as they are inherently mutable +var ( + // magic bytes to identify gzip. + // See https://www.ietf.org/rfc/rfc1952.txt + // and https://github.com/golang/go/blob/master/src/net/http/sniff.go#L186 + gzipIdent = []byte("\x1F\x8B\x08") + + wasmIdent = []byte("\x00\x61\x73\x6D") +) + +// IsGzip returns checks if the file contents are gzip compressed +func IsGzip(input []byte) bool { + return len(input) >= 3 && bytes.Equal(gzipIdent, input[0:3]) +} + +// IsWasm checks if the file contents are of wasm binary +func IsWasm(input []byte) bool { + return bytes.Equal(input[:4], wasmIdent) +} + +// GzipIt compresses the input ([]byte) +func GzipIt(input []byte) ([]byte, error) { + // Create gzip writer. + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write(input) + if err != nil { + return nil, err + } + err = w.Close() // You must close this first to flush the bytes to the buffer. + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} diff --git a/x/wasm/ioutils/utils_test.go b/x/wasm/ioutils/utils_test.go new file mode 100644 index 00000000..2dea0c5c --- /dev/null +++ b/x/wasm/ioutils/utils_test.go @@ -0,0 +1,68 @@ +package ioutils + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func GetTestData() ([]byte, []byte, []byte, error) { + wasmCode, err := os.ReadFile("../keeper/testdata/hackatom.wasm") + if err != nil { + return nil, nil, nil, err + } + + gzipData, err := GzipIt(wasmCode) + if err != nil { + return nil, nil, nil, err + } + + someRandomStr := []byte("hello world") + + return wasmCode, someRandomStr, gzipData, nil +} + +func TestIsWasm(t *testing.T) { + wasmCode, someRandomStr, gzipData, err := GetTestData() + require.NoError(t, err) + + t.Log("should return false for some random string data") + require.False(t, IsWasm(someRandomStr)) + t.Log("should return false for gzip data") + require.False(t, IsWasm(gzipData)) + t.Log("should return true for exact wasm") + require.True(t, IsWasm(wasmCode)) +} + +func TestIsGzip(t *testing.T) { + wasmCode, someRandomStr, gzipData, err := GetTestData() + require.NoError(t, err) + + require.False(t, IsGzip(wasmCode)) + require.False(t, IsGzip(someRandomStr)) + require.False(t, IsGzip(nil)) + require.True(t, IsGzip(gzipData[0:3])) + require.True(t, IsGzip(gzipData)) +} + +func TestGzipIt(t *testing.T) { + wasmCode, someRandomStr, _, err := GetTestData() + originalGzipData := []byte{ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 202, 72, 205, 201, 201, 87, 40, 207, 47, 202, 73, 1, + 4, 0, 0, 255, 255, 133, 17, 74, 13, 11, 0, 0, 0, + } + + require.NoError(t, err) + + t.Log("gzip wasm with no error") + _, err = GzipIt(wasmCode) + require.NoError(t, err) + + t.Log("gzip of a string should return exact gzip data") + strToGzip, err := GzipIt(someRandomStr) + + require.True(t, IsGzip(strToGzip)) + require.NoError(t, err) + require.Equal(t, originalGzipData, strToGzip) +} diff --git a/x/wasm/keeper/addresses.go b/x/wasm/keeper/addresses.go new file mode 100644 index 00000000..390a24f6 --- /dev/null +++ b/x/wasm/keeper/addresses.go @@ -0,0 +1,76 @@ +package keeper + +import ( + "encoding/binary" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// AddressGenerator abstract address generator to be used for a single contract address +type AddressGenerator func(ctx sdk.Context, codeID uint64, checksum []byte) sdk.AccAddress + +// ClassicAddressGenerator generates a contract address using codeID and instanceID sequence +func (k Keeper) ClassicAddressGenerator() AddressGenerator { + return func(ctx sdk.Context, codeID uint64, _ []byte) sdk.AccAddress { + instanceID := k.autoIncrementID(ctx, types.KeyLastInstanceID) + return BuildContractAddressClassic(codeID, instanceID) + } +} + +// PredicableAddressGenerator generates a predictable contract address +func PredicableAddressGenerator(creator sdk.AccAddress, salt []byte, msg []byte, fixMsg bool) AddressGenerator { + return func(ctx sdk.Context, _ uint64, checksum []byte) sdk.AccAddress { + if !fixMsg { // clear msg to not be included in the address generation + msg = []byte{} + } + return BuildContractAddressPredictable(checksum, creator, salt, msg) + } +} + +// BuildContractAddressClassic builds an sdk account address for a contract. +func BuildContractAddressClassic(codeID, instanceID uint64) sdk.AccAddress { + contractID := make([]byte, 16) + binary.BigEndian.PutUint64(contractID[:8], codeID) + binary.BigEndian.PutUint64(contractID[8:], instanceID) + return address.Module(types.ModuleName, contractID)[:types.ContractAddrLen] +} + +// BuildContractAddressPredictable generates a contract address for the wasm module with len = types.ContractAddrLen using the +// Cosmos SDK address.Module function. +// Internally a key is built containing: +// (len(checksum) | checksum | len(sender_address) | sender_address | len(salt) | salt| len(initMsg) | initMsg). +// +// All method parameter values must be valid and not nil. +func BuildContractAddressPredictable(checksum []byte, creator sdk.AccAddress, salt, initMsg types.RawContractMessage) sdk.AccAddress { + if len(checksum) != 32 { + panic("invalid checksum") + } + if err := sdk.VerifyAddressFormat(creator); err != nil { + panic(fmt.Sprintf("creator: %s", err)) + } + if err := types.ValidateSalt(salt); err != nil { + panic(fmt.Sprintf("salt: %s", err)) + } + if err := initMsg.ValidateBasic(); len(initMsg) != 0 && err != nil { + panic(fmt.Sprintf("initMsg: %s", err)) + } + checksum = UInt64LengthPrefix(checksum) + creator = UInt64LengthPrefix(creator) + salt = UInt64LengthPrefix(salt) + initMsg = UInt64LengthPrefix(initMsg) + key := make([]byte, len(checksum)+len(creator)+len(salt)+len(initMsg)) + copy(key[0:], checksum) + copy(key[len(checksum):], creator) + copy(key[len(checksum)+len(creator):], salt) + copy(key[len(checksum)+len(creator)+len(salt):], initMsg) + return address.Module(types.ModuleName, key)[:types.ContractAddrLen] +} + +// UInt64LengthPrefix prepend big endian encoded byte length +func UInt64LengthPrefix(bz []byte) []byte { + return append(sdk.Uint64ToBigEndian(uint64(len(bz))), bz...) +} diff --git a/x/wasm/keeper/addresses_test.go b/x/wasm/keeper/addresses_test.go new file mode 100644 index 00000000..fbcc607f --- /dev/null +++ b/x/wasm/keeper/addresses_test.go @@ -0,0 +1,432 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + tmbytes "github.com/tendermint/tendermint/libs/bytes" +) + +func TestBuildContractAddress(t *testing.T) { + x, y := sdk.GetConfig().GetBech32AccountAddrPrefix(), sdk.GetConfig().GetBech32AccountPubPrefix() + t.Cleanup(func() { + sdk.GetConfig().SetBech32PrefixForAccount(x, y) + }) + sdk.GetConfig().SetBech32PrefixForAccount("purple", "purple") + + // test vectors generated via cosmjs: https://github.com/cosmos/cosmjs/pull/1253/files + type Spec struct { + In struct { + Checksum tmbytes.HexBytes `json:"checksum"` + Creator sdk.AccAddress `json:"creator"` + Salt tmbytes.HexBytes `json:"salt"` + Msg string `json:"msg"` + } `json:"in"` + Out struct { + Address sdk.AccAddress `json:"address"` + } `json:"out"` + } + var specs []Spec + require.NoError(t, json.Unmarshal([]byte(goldenMasterPredictableContractAddr), &specs)) + require.NotEmpty(t, specs) + for i, spec := range specs { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + // when + gotAddr := BuildContractAddressPredictable(spec.In.Checksum, spec.In.Creator, spec.In.Salt.Bytes(), []byte(spec.In.Msg)) + + require.Equal(t, spec.Out.Address.String(), gotAddr.String()) + require.NoError(t, sdk.VerifyAddressFormat(gotAddr)) + }) + } +} + +const goldenMasterPredictableContractAddr = `[ + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000001610000000000000000", + "addressData": "5e865d3e45ad3e961f77fd77d46543417ced44d924dc3e079b5415ff6775f847" + }, + "out": { + "address": "purple1t6r960j945lfv8mhl4mage2rg97w63xeynwrupum2s2l7em4lprs9ce5hk" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc00000000000000016100000000000000027b7d", + "addressData": "0995499608947a5281e2c7ebd71bdb26a1ad981946dad57f6c4d3ee35de77835" + }, + "out": { + "address": "purple1px25n9sgj3a99q0zcl4awx7my6s6mxqegmdd2lmvf5lwxh080q6suttktr" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "83326e554723b15bac664ceabc8a5887e27003abe9fbd992af8c7bcea4745167" + }, + "out": { + "address": "purple1svexu428ywc4htrxfn4tezjcsl38qqata8aany4033auafr529ns4v254c" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "9384c6248c0bb171e306fd7da0993ec1e20eba006452a3a9e078883eb3594564" + }, + "out": { + "address": "purple1jwzvvfyvpwchrccxl476pxf7c83qawsqv3f2820q0zyrav6eg4jqdcq7gc" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "9a8d5f98fb186825401a26206158e7a1213311a9b6a87944469913655af52ffb" + }, + "out": { + "address": "purple1n2x4lx8mrp5z2sq6ycsxzk885ysnxydfk658j3zxnyfk2kh49lasesxf6j" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "932f07bc53f7d0b0b43cb5a54ac3e245b205e6ae6f7c1d991dc6af4a2ff9ac18" + }, + "out": { + "address": "purple1jvhs00zn7lgtpdpukkj54slzgkeqte4wda7pmxgac6h55tle4svq8cmp60" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000001610000000000000000", + "addressData": "9725e94f528d8b78d33c25f3dfcd60e6142d8be60ab36f6a5b59036fd51560db" + }, + "out": { + "address": "purple1juj7jn6j3k9h35euyhealntquc2zmzlxp2ek76jmtypkl4g4vrdsfwmwxk" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff00000000000000016100000000000000027b7d", + "addressData": "b056e539bbaf447ba18f3f13b792970111fc78933eb6700f4d227b5216d63658" + }, + "out": { + "address": "purple1kptw2wdm4az8hgv08ufm0y5hqyglc7yn86m8qr6dyfa4y9kkxevqmkm9q3" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "6c98434180f052294ff89fb6d2dae34f9f4468b0b8e6e7c002b2a44adee39abd" + }, + "out": { + "address": "purple1djvyxsvq7pfzjnlcn7md9khrf705g69shrnw0sqzk2jy4hhrn27sjh2ysy" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "0aaf1c31c5d529d21d898775bc35b3416f47bfd99188c334c6c716102cbd3101" + }, + "out": { + "address": "purple1p2h3cvw9655ay8vfsa6mcddng9h5007ejxyvxdxxcutpqt9axyqsagmmay" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "36fe6ab732187cdfed46290b448b32eb7f4798e7a4968b0537de8a842cbf030e" + }, + "out": { + "address": "purple1xmlx4dejrp7dlm2x9y95fzejadl50x885jtgkpfhm69ggt9lqv8qk3vn4f" + } + }, + { + "in": { + "checksum": "13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d00000000000000002013a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a500000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "a0d0c942adac6f3e5e7c23116c4c42a24e96e0ab75f53690ec2d3de16067c751" + }, + "out": { + "address": "purple15rgvjs4d43hnuhnuyvgkcnzz5f8fdc9twh6ndy8v9577zcr8cags40l9dt" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000001610000000000000000", + "addressData": "b95c467218d408a0f93046f227b6ece7fe18133ff30113db4d2a7becdfeca141" + }, + "out": { + "address": "purple1h9wyvusc6sy2p7fsgmez0dhvullpsyel7vq38k6d9fa7ehlv59qsvnyh36" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc00000000000000016100000000000000027b7d", + "addressData": "23fe45dbbd45dc6cd25244a74b6e99e7a65bf0bac2f2842a05049d37555a3ae6" + }, + "out": { + "address": "purple1y0lytkaaghwxe5jjgjn5km5eu7n9hu96ctegg2s9qjwnw4268tnqxhg60a" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "6faea261ed63baa65b05726269e83b217fa6205dc7d9fb74f9667d004a69c082" + }, + "out": { + "address": "purple1d7h2yc0dvwa2vkc9wf3xn6pmy9l6vgzaclvlka8eve7sqjnfczpqqsdnwu" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "67a3ab6384729925fdb144574628ce96836fe098d2c6be4e84ac106b2728d96c" + }, + "out": { + "address": "purple1v736kcuyw2vjtld3g3t5v2xwj6pklcyc6trtun5y4sgxkfegm9kq7vgpnt" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "23a121263bfce05c144f4af86f3d8a9f87dc56f9dc48dbcffc8c5a614da4c661" + }, + "out": { + "address": "purple1ywsjzf3mlns9c9z0ftux70v2n7rac4hem3ydhnlu33dxzndycesssc7x2m" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbcccccccccc", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000149999999999aaaaaaaaaabbbbbbbbbbcccccccccc0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "dd90dba6d6fcd5fb9c9c8f536314eb1bb29cb5aa084b633c5806b926a5636b58" + }, + "out": { + "address": "purple1mkgdhfkkln2lh8yu3afkx98trwefedd2pp9kx0zcq6ujdftrddvq50esay" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000001610000000000000000", + "addressData": "547a743022f4f1af05b102f57bf1c1c7d7ee81bae427dc20d36b2c4ec57612ae" + }, + "out": { + "address": "purple123a8gvpz7nc67pd3qt6hhuwpclt7aqd6usnacgxndvkya3tkz2hq5hz38f" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff00000000000000016100000000000000027b7d", + "addressData": "416e169110e4b411bc53162d7503b2bbf14d6b36b1413a4f4c9af622696e9665" + }, + "out": { + "address": "purple1g9hpdygsuj6pr0znzckh2qajh0c566ekk9qn5n6vntmzy6twjejsrl9alk" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "61", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff000000000000000161000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "619a0988b92d8796cea91dea63cbb1f1aefa4a6b6ee5c5d1e937007252697220" + }, + "out": { + "address": "purple1vxdqnz9e9kredn4frh4x8ja37xh05jntdmjut50fxuq8y5nfwgsquu9mxh" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": null + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae0000000000000000", + "addressData": "d8af856a6a04852d19b647ad6d4336eb26e077f740aef1a0331db34d299a885a" + }, + "out": { + "address": "purple1mzhc26n2qjzj6xdkg7kk6sekavnwqalhgzh0rgpnrke562v63pdq8grp8q" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae00000000000000027b7d", + "addressData": "c7fb7bea96daab23e416c4fcf328215303005e1d0d5424257335568e5381e33c" + }, + "out": { + "address": "purple1clahh65km24j8eqkcn70x2pp2vpsqhsap42zgftnx4tgu5upuv7q9ywjws" + } + }, + { + "in": { + "checksum": "1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b", + "creator": "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m", + "creatorData": "9999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff", + "salt": "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + "msg": "{\"some\":123,\"structure\":{\"nested\":[\"ok\",true]}}" + }, + "intermediate": { + "key": "7761736d0000000000000000201da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b00000000000000209999999999aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffff0000000000000040aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae000000000000002f7b22736f6d65223a3132332c22737472756374757265223a7b226e6573746564223a5b226f6b222c747275655d7d7d", + "addressData": "ccdf9dea141a6c2475870529ab38fae9dec30df28e005894fe6578b66133ab4a" + }, + "out": { + "address": "purple1en0em6s5rfkzgav8q556kw86a80vxr0j3cq93987v4utvcfn4d9q0tql4w" + } + } +] +` diff --git a/x/wasm/keeper/ante.go b/x/wasm/keeper/ante.go new file mode 100644 index 00000000..e291233e --- /dev/null +++ b/x/wasm/keeper/ante.go @@ -0,0 +1,96 @@ +package keeper + +import ( + "encoding/binary" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// CountTXDecorator ante handler to count the tx position in a block. +type CountTXDecorator struct { + storeKey sdk.StoreKey +} + +// NewCountTXDecorator constructor +func NewCountTXDecorator(storeKey sdk.StoreKey) *CountTXDecorator { + return &CountTXDecorator{storeKey: storeKey} +} + +// AnteHandle handler stores a tx counter with current height encoded in the store to let the app handle +// global rollback behavior instead of keeping state in the handler itself. +// The ante handler passes the counter value via sdk.Context upstream. See `types.TXCounter(ctx)` to read the value. +// Simulations don't get a tx counter value assigned. +func (a CountTXDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if simulate { + return next(ctx, tx, simulate) + } + store := ctx.KVStore(a.storeKey) + currentHeight := ctx.BlockHeight() + + var txCounter uint32 // start with 0 + // load counter when exists + if bz := store.Get(types.TXCounterPrefix); bz != nil { + lastHeight, val := decodeHeightCounter(bz) + if currentHeight == lastHeight { + // then use stored counter + txCounter = val + } // else use `0` from above to start with + } + // store next counter value for current height + store.Set(types.TXCounterPrefix, encodeHeightCounter(currentHeight, txCounter+1)) + + return next(types.WithTXCounter(ctx, txCounter), tx, simulate) +} + +func encodeHeightCounter(height int64, counter uint32) []byte { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, counter) + return append(sdk.Uint64ToBigEndian(uint64(height)), b...) +} + +func decodeHeightCounter(bz []byte) (int64, uint32) { + return int64(sdk.BigEndianToUint64(bz[0:8])), binary.BigEndian.Uint32(bz[8:]) +} + +// LimitSimulationGasDecorator ante decorator to limit gas in simulation calls +type LimitSimulationGasDecorator struct { + gasLimit *sdk.Gas +} + +// NewLimitSimulationGasDecorator constructor accepts nil value to fallback to block gas limit. +func NewLimitSimulationGasDecorator(gasLimit *sdk.Gas) *LimitSimulationGasDecorator { + if gasLimit != nil && *gasLimit == 0 { + panic("gas limit must not be zero") + } + return &LimitSimulationGasDecorator{gasLimit: gasLimit} +} + +// AnteHandle that limits the maximum gas available in simulations only. +// A custom max value can be configured and will be applied when set. The value should not +// exceed the max block gas limit. +// Different values on nodes are not consensus breaking as they affect only +// simulations but may have effect on client user experience. +// +// When no custom value is set then the max block gas is used as default limit. +func (d LimitSimulationGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if !simulate { + // Wasm code is not executed in checkTX so that we don't need to limit it further. + // Tendermint rejects the TX afterwards when the tx.gas > max block gas. + // On deliverTX we rely on the tendermint/sdk mechanics that ensure + // tx has gas set and gas < max block gas + return next(ctx, tx, simulate) + } + + // apply custom node gas limit + if d.gasLimit != nil { + return next(ctx.WithGasMeter(sdk.NewGasMeter(*d.gasLimit)), tx, simulate) + } + + // default to max block gas when set, to be on the safe side + if maxGas := ctx.ConsensusParams().GetBlock().MaxGas; maxGas > 0 { + return next(ctx.WithGasMeter(sdk.NewGasMeter(sdk.Gas(maxGas))), tx, simulate) + } + return next(ctx, tx, simulate) +} diff --git a/x/wasm/keeper/ante_test.go b/x/wasm/keeper/ante_test.go new file mode 100644 index 00000000..5c3d7701 --- /dev/null +++ b/x/wasm/keeper/ante_test.go @@ -0,0 +1,189 @@ +package keeper_test + +import ( + "testing" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/keeper" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestCountTxDecorator(t *testing.T) { + keyWasm := sdk.NewKVStoreKey(types.StoreKey) + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyWasm, sdk.StoreTypeIAVL, db) + require.NoError(t, ms.LoadLatestVersion()) + const myCurrentBlockHeight = 100 + + specs := map[string]struct { + setupDB func(t *testing.T, ctx sdk.Context) + simulate bool + nextAssertAnte func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) + expErr bool + }{ + "no initial counter set": { + setupDB: func(t *testing.T, ctx sdk.Context) {}, + nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + gotCounter, ok := types.TXCounter(ctx) + require.True(t, ok) + assert.Equal(t, uint32(0), gotCounter) + // and stored +1 + bz := ctx.MultiStore().GetKVStore(keyWasm).Get(types.TXCounterPrefix) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 0, 0, 0, 1}, bz) + return ctx, nil + }, + }, + "persistent counter incremented - big endian": { + setupDB: func(t *testing.T, ctx sdk.Context) { + bz := []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 1, 0, 0, 2} + ctx.MultiStore().GetKVStore(keyWasm).Set(types.TXCounterPrefix, bz) + }, + nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + gotCounter, ok := types.TXCounter(ctx) + require.True(t, ok) + assert.Equal(t, uint32(1<<24+2), gotCounter) + // and stored +1 + bz := ctx.MultiStore().GetKVStore(keyWasm).Get(types.TXCounterPrefix) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 1, 0, 0, 3}, bz) + return ctx, nil + }, + }, + "old height counter replaced": { + setupDB: func(t *testing.T, ctx sdk.Context) { + previousHeight := byte(myCurrentBlockHeight - 1) + bz := []byte{0, 0, 0, 0, 0, 0, 0, previousHeight, 0, 0, 0, 1} + ctx.MultiStore().GetKVStore(keyWasm).Set(types.TXCounterPrefix, bz) + }, + nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + gotCounter, ok := types.TXCounter(ctx) + require.True(t, ok) + assert.Equal(t, uint32(0), gotCounter) + // and stored +1 + bz := ctx.MultiStore().GetKVStore(keyWasm).Get(types.TXCounterPrefix) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, myCurrentBlockHeight, 0, 0, 0, 1}, bz) + return ctx, nil + }, + }, + "simulation not persisted": { + setupDB: func(t *testing.T, ctx sdk.Context) { + }, + simulate: true, + nextAssertAnte: func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + _, ok := types.TXCounter(ctx) + assert.False(t, ok) + require.True(t, simulate) + // and not stored + assert.False(t, ctx.MultiStore().GetKVStore(keyWasm).Has(types.TXCounterPrefix)) + return ctx, nil + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx := sdk.NewContext(ms.CacheMultiStore(), tmproto.Header{ + Height: myCurrentBlockHeight, + Time: time.Date(2021, time.September, 27, 12, 0, 0, 0, time.UTC), + }, false, log.NewNopLogger()) + + spec.setupDB(t, ctx) + var anyTx sdk.Tx + + // when + ante := keeper.NewCountTXDecorator(keyWasm) + _, gotErr := ante.AnteHandle(ctx, anyTx, spec.simulate, spec.nextAssertAnte) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestLimitSimulationGasDecorator(t *testing.T) { + var ( + hundred sdk.Gas = 100 + zero sdk.Gas = 0 + ) + specs := map[string]struct { + customLimit *sdk.Gas + consumeGas sdk.Gas + maxBlockGas int64 + simulation bool + expErr interface{} + }{ + "custom limit set": { + customLimit: &hundred, + consumeGas: hundred + 1, + maxBlockGas: -1, + simulation: true, + expErr: sdk.ErrorOutOfGas{Descriptor: "testing"}, + }, + "block limit set": { + maxBlockGas: 100, + consumeGas: hundred + 1, + simulation: true, + expErr: sdk.ErrorOutOfGas{Descriptor: "testing"}, + }, + "no limits set": { + maxBlockGas: -1, + consumeGas: hundred + 1, + simulation: true, + }, + "both limits set, custom applies": { + customLimit: &hundred, + consumeGas: hundred - 1, + maxBlockGas: 10, + simulation: true, + }, + "not a simulation": { + customLimit: &hundred, + consumeGas: hundred + 1, + simulation: false, + }, + "zero custom limit": { + customLimit: &zero, + simulation: true, + expErr: "gas limit must not be zero", + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + nextAnte := consumeGasAnteHandler(spec.consumeGas) + ctx := sdk.Context{}. + WithGasMeter(sdk.NewInfiniteGasMeter()). + WithConsensusParams(&abci.ConsensusParams{ + Block: &abci.BlockParams{MaxGas: spec.maxBlockGas}, + }) + // when + if spec.expErr != nil { + require.PanicsWithValue(t, spec.expErr, func() { + ante := keeper.NewLimitSimulationGasDecorator(spec.customLimit) + ante.AnteHandle(ctx, nil, spec.simulation, nextAnte) + }) + return + } + ante := keeper.NewLimitSimulationGasDecorator(spec.customLimit) + ante.AnteHandle(ctx, nil, spec.simulation, nextAnte) + }) + } +} + +func consumeGasAnteHandler(gasToConsume sdk.Gas) sdk.AnteHandler { + return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + ctx.GasMeter().ConsumeGas(gasToConsume, "testing") + return ctx, nil + } +} diff --git a/x/wasm/keeper/api.go b/x/wasm/keeper/api.go new file mode 100644 index 00000000..3b39b34d --- /dev/null +++ b/x/wasm/keeper/api.go @@ -0,0 +1,43 @@ +package keeper + +import ( + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // DefaultGasCostHumanAddress is how moch SDK gas we charge to convert to a human address format + DefaultGasCostHumanAddress = 5 + // DefaultGasCostCanonicalAddress is how moch SDK gas we charge to convert to a canonical address format + DefaultGasCostCanonicalAddress = 4 + + // DefaultDeserializationCostPerByte The formular should be `len(data) * deserializationCostPerByte` + DefaultDeserializationCostPerByte = 1 +) + +var ( + costHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier + costCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier + costJSONDeserialization = wasmvmtypes.UFraction{ + Numerator: DefaultDeserializationCostPerByte * DefaultGasMultiplier, + Denominator: 1, + } +) + +func humanAddress(canon []byte) (string, uint64, error) { + if err := sdk.VerifyAddressFormat(canon); err != nil { + return "", costHumanize, err + } + return sdk.AccAddress(canon).String(), costHumanize, nil +} + +func canonicalAddress(human string) ([]byte, uint64, error) { + bz, err := sdk.AccAddressFromBech32(human) + return bz, costCanonical, err +} + +var cosmwasmAPI = wasmvm.GoAPI{ + HumanAddress: humanAddress, + CanonicalAddress: canonicalAddress, +} diff --git a/x/wasm/keeper/authz_policy.go b/x/wasm/keeper/authz_policy.go new file mode 100644 index 00000000..0c5fb77f --- /dev/null +++ b/x/wasm/keeper/authz_policy.go @@ -0,0 +1,63 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// ChainAccessConfigs chain settings +type ChainAccessConfigs struct { + Upload types.AccessConfig + Instantiate types.AccessConfig +} + +// NewChainAccessConfigs constructor +func NewChainAccessConfigs(upload types.AccessConfig, instantiate types.AccessConfig) ChainAccessConfigs { + return ChainAccessConfigs{Upload: upload, Instantiate: instantiate} +} + +type AuthorizationPolicy interface { + CanCreateCode(chainConfigs ChainAccessConfigs, actor sdk.AccAddress, contractConfig types.AccessConfig) bool + CanInstantiateContract(c types.AccessConfig, actor sdk.AccAddress) bool + CanModifyContract(admin, actor sdk.AccAddress) bool + CanModifyCodeAccessConfig(creator, actor sdk.AccAddress, isSubset bool) bool +} + +type DefaultAuthorizationPolicy struct{} + +func (p DefaultAuthorizationPolicy) CanCreateCode(chainConfigs ChainAccessConfigs, actor sdk.AccAddress, contractConfig types.AccessConfig) bool { + return chainConfigs.Upload.Allowed(actor) && + contractConfig.IsSubset(chainConfigs.Instantiate) +} + +func (p DefaultAuthorizationPolicy) CanInstantiateContract(config types.AccessConfig, actor sdk.AccAddress) bool { + return config.Allowed(actor) +} + +func (p DefaultAuthorizationPolicy) CanModifyContract(admin, actor sdk.AccAddress) bool { + return admin != nil && admin.Equals(actor) +} + +func (p DefaultAuthorizationPolicy) CanModifyCodeAccessConfig(creator, actor sdk.AccAddress, isSubset bool) bool { + return creator != nil && creator.Equals(actor) && isSubset +} + +type GovAuthorizationPolicy struct{} + +// CanCreateCode implements AuthorizationPolicy.CanCreateCode to allow gov actions. Always returns true. +func (p GovAuthorizationPolicy) CanCreateCode(ChainAccessConfigs, sdk.AccAddress, types.AccessConfig) bool { + return true +} + +func (p GovAuthorizationPolicy) CanInstantiateContract(types.AccessConfig, sdk.AccAddress) bool { + return true +} + +func (p GovAuthorizationPolicy) CanModifyContract(sdk.AccAddress, sdk.AccAddress) bool { + return true +} + +func (p GovAuthorizationPolicy) CanModifyCodeAccessConfig(sdk.AccAddress, sdk.AccAddress, bool) bool { + return true +} diff --git a/x/wasm/keeper/authz_policy_test.go b/x/wasm/keeper/authz_policy_test.go new file mode 100644 index 00000000..d7fccbdd --- /dev/null +++ b/x/wasm/keeper/authz_policy_test.go @@ -0,0 +1,345 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestDefaultAuthzPolicyCanCreateCode(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + specs := map[string]struct { + chainConfigs ChainAccessConfigs + contractInstConf types.AccessConfig + actor sdk.AccAddress + exp bool + panics bool + }{ + "upload nobody": { + chainConfigs: NewChainAccessConfigs(types.AllowNobody, types.AllowEverybody), + contractInstConf: types.AllowEverybody, + exp: false, + }, + "upload everybody": { + chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody), + contractInstConf: types.AllowEverybody, + exp: true, + }, + "upload only address - same": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(myActorAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + exp: true, + }, + "upload only address - different": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(otherAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + exp: false, + }, + "upload any address - included": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + exp: true, + }, + "upload any address - not included": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + exp: false, + }, + "contract config - subtype": { + chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody), + contractInstConf: types.AccessTypeAnyOfAddresses.With(myActorAddress), + exp: true, + }, + "contract config - not subtype": { + chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowNobody), + contractInstConf: types.AllowEverybody, + exp: false, + }, + "upload undefined config - panics": { + chainConfigs: NewChainAccessConfigs(types.AccessConfig{}, types.AllowEverybody), + contractInstConf: types.AllowEverybody, + panics: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := DefaultAuthorizationPolicy{} + if !spec.panics { + got := policy.CanCreateCode(spec.chainConfigs, myActorAddress, spec.contractInstConf) + assert.Equal(t, spec.exp, got) + return + } + assert.Panics(t, func() { + policy.CanCreateCode(spec.chainConfigs, myActorAddress, spec.contractInstConf) + }) + }) + } +} + +func TestDefaultAuthzPolicyCanInstantiateContract(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + specs := map[string]struct { + config types.AccessConfig + actor sdk.AccAddress + exp bool + panics bool + }{ + "nobody": { + config: types.AllowNobody, + exp: false, + }, + "everybody": { + config: types.AllowEverybody, + exp: true, + }, + "only address - same": { + config: types.AccessTypeOnlyAddress.With(myActorAddress), + exp: true, + }, + "only address - different": { + config: types.AccessTypeOnlyAddress.With(otherAddress), + exp: false, + }, + "any address - included": { + config: types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress), + exp: true, + }, + "any address - not included": { + config: types.AccessTypeAnyOfAddresses.With(otherAddress), + exp: false, + }, + "undefined config - panics": { + config: types.AccessConfig{}, + panics: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := DefaultAuthorizationPolicy{} + if !spec.panics { + got := policy.CanInstantiateContract(spec.config, myActorAddress) + assert.Equal(t, spec.exp, got) + return + } + assert.Panics(t, func() { + policy.CanInstantiateContract(spec.config, myActorAddress) + }) + }) + } +} + +func TestDefaultAuthzPolicyCanModifyContract(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + + specs := map[string]struct { + admin sdk.AccAddress + exp bool + }{ + "same as actor": { + admin: myActorAddress, + exp: true, + }, + "different admin": { + admin: otherAddress, + exp: false, + }, + "no admin": { + exp: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := DefaultAuthorizationPolicy{} + got := policy.CanModifyContract(spec.admin, myActorAddress) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestDefaultAuthzPolicyCanModifyCodeAccessConfig(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + + specs := map[string]struct { + admin sdk.AccAddress + subset bool + exp bool + }{ + "same as actor - subset": { + admin: myActorAddress, + subset: true, + exp: true, + }, + "same as actor - not subset": { + admin: myActorAddress, + subset: false, + exp: false, + }, + "different admin": { + admin: otherAddress, + exp: false, + }, + "no admin": { + exp: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := DefaultAuthorizationPolicy{} + got := policy.CanModifyCodeAccessConfig(spec.admin, myActorAddress, spec.subset) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestGovAuthzPolicyCanCreateCode(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + specs := map[string]struct { + chainConfigs ChainAccessConfigs + contractInstConf types.AccessConfig + actor sdk.AccAddress + }{ + "upload nobody": { + chainConfigs: NewChainAccessConfigs(types.AllowNobody, types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + "upload everybody": { + chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + "upload only address - same": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(myActorAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + "upload only address - different": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeOnlyAddress.With(otherAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + "upload any address - included": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + "upload any address - not included": { + chainConfigs: NewChainAccessConfigs(types.AccessTypeAnyOfAddresses.With(otherAddress), types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + "contract config - subtype": { + chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowEverybody), + contractInstConf: types.AccessTypeAnyOfAddresses.With(myActorAddress), + }, + "contract config - not subtype": { + chainConfigs: NewChainAccessConfigs(types.AllowEverybody, types.AllowNobody), + contractInstConf: types.AllowEverybody, + }, + "upload undefined config - not panics": { + chainConfigs: NewChainAccessConfigs(types.AccessConfig{}, types.AllowEverybody), + contractInstConf: types.AllowEverybody, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := GovAuthorizationPolicy{} + got := policy.CanCreateCode(spec.chainConfigs, myActorAddress, spec.contractInstConf) + assert.True(t, got) + }) + } +} + +func TestGovAuthzPolicyCanInstantiateContract(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + specs := map[string]struct { + config types.AccessConfig + actor sdk.AccAddress + }{ + "nobody": { + config: types.AllowNobody, + }, + "everybody": { + config: types.AllowEverybody, + }, + "only address - same": { + config: types.AccessTypeOnlyAddress.With(myActorAddress), + }, + "only address - different": { + config: types.AccessTypeOnlyAddress.With(otherAddress), + }, + "any address - included": { + config: types.AccessTypeAnyOfAddresses.With(otherAddress, myActorAddress), + }, + "any address - not included": { + config: types.AccessTypeAnyOfAddresses.With(otherAddress), + }, + "undefined config - panics": { + config: types.AccessConfig{}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := GovAuthorizationPolicy{} + got := policy.CanInstantiateContract(spec.config, myActorAddress) + assert.True(t, got) + }) + } +} + +func TestGovAuthzPolicyCanModifyContract(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + + specs := map[string]struct { + admin sdk.AccAddress + }{ + "same as actor": { + admin: myActorAddress, + }, + "different admin": { + admin: otherAddress, + }, + "no admin": {}, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := GovAuthorizationPolicy{} + got := policy.CanModifyContract(spec.admin, myActorAddress) + assert.True(t, got) + }) + } +} + +func TestGovAuthzPolicyCanModifyCodeAccessConfig(t *testing.T) { + myActorAddress := RandomAccountAddress(t) + otherAddress := RandomAccountAddress(t) + + specs := map[string]struct { + admin sdk.AccAddress + subset bool + }{ + "same as actor - subset": { + admin: myActorAddress, + subset: true, + }, + "same as actor - not subset": { + admin: myActorAddress, + subset: false, + }, + "different admin": { + admin: otherAddress, + }, + "no admin": {}, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + policy := GovAuthorizationPolicy{} + got := policy.CanModifyCodeAccessConfig(spec.admin, myActorAddress, spec.subset) + assert.True(t, got) + }) + } +} diff --git a/x/wasm/keeper/bench_test.go b/x/wasm/keeper/bench_test.go new file mode 100644 index 00000000..3e850e61 --- /dev/null +++ b/x/wasm/keeper/bench_test.go @@ -0,0 +1,102 @@ +package keeper + +import ( + "os" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// BenchmarkVerification benchmarks secp256k1 verification which is 1000 gas based on cpu time. +// +// Just this function is copied from +// https://github.com/cosmos/cosmos-sdk/blob/90e9370bd80d9a3d41f7203ddb71166865561569/crypto/keys/internal/benchmarking/bench.go#L48-L62 +// And thus under the GO license (BSD style) +func BenchmarkGasNormalization(b *testing.B) { + priv := secp256k1.GenPrivKey() + pub := priv.PubKey() + + // use a short message, so this time doesn't get dominated by hashing. + message := []byte("Hello, world!") + signature, err := priv.Sign(message) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + pub.VerifySignature(message, signature) + } +} + +// By comparing the timing for queries on pinned vs unpinned, the difference gives us the overhead of +// instantiating an unpinned contract. That value can be used to determine a reasonable gas price +// for the InstantiationCost +func BenchmarkInstantiationOverhead(b *testing.B) { + specs := map[string]struct { + pinned bool + db func() dbm.DB + }{ + "unpinned, memory db": { + db: func() dbm.DB { return dbm.NewMemDB() }, + }, + "pinned, memory db": { + db: func() dbm.DB { return dbm.NewMemDB() }, + pinned: true, + }, + } + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + wasmConfig := types.WasmConfig{MemoryCacheSize: 0} + ctx, keepers := createTestInput(b, false, AvailableCapabilities, wasmConfig, spec.db()) + example := InstantiateHackatomExampleContract(b, ctx, keepers) + if spec.pinned { + require.NoError(b, keepers.ContractKeeper.PinCode(ctx, example.CodeID)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := keepers.WasmKeeper.QuerySmart(ctx, example.Contract, []byte(`{"verifier":{}}`)) + require.NoError(b, err) + } + }) + } +} + +// Calculate the time it takes to compile some wasm code the first time. +// This will help us adjust pricing for UploadCode +func BenchmarkCompilation(b *testing.B) { + specs := map[string]struct { + wasmFile string + }{ + "hackatom": { + wasmFile: "./testdata/hackatom.wasm", + }, + "burner": { + wasmFile: "./testdata/burner.wasm", + }, + "ibc_reflect": { + wasmFile: "./testdata/ibc_reflect.wasm", + }, + } + + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + wasmConfig := types.WasmConfig{MemoryCacheSize: 0} + db := dbm.NewMemDB() + ctx, keepers := createTestInput(b, false, AvailableCapabilities, wasmConfig, db) + + // print out code size for comparisons + code, err := os.ReadFile(spec.wasmFile) + require.NoError(b, err) + b.Logf("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b(size: %d) ", len(code)) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = StoreExampleContract(b, ctx, keepers, spec.wasmFile) + } + }) + } +} diff --git a/x/wasm/keeper/contract_keeper.go b/x/wasm/keeper/contract_keeper.go new file mode 100644 index 00000000..efd0038d --- /dev/null +++ b/x/wasm/keeper/contract_keeper.go @@ -0,0 +1,130 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ types.ContractOpsKeeper = PermissionedKeeper{} + +// decoratedKeeper contains a subset of the wasm keeper that are already or can be guarded by an authorization policy in the future +type decoratedKeeper interface { + create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, instantiateAccess *types.AccessConfig, authZ AuthorizationPolicy) (codeID uint64, checksum []byte, err error) + + instantiate( + ctx sdk.Context, + codeID uint64, + creator, admin sdk.AccAddress, + initMsg []byte, + label string, + deposit sdk.Coins, + addressGenerator AddressGenerator, + authZ AuthorizationPolicy, + ) (sdk.AccAddress, []byte, error) + + migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte, authZ AuthorizationPolicy) ([]byte, error) + setContractAdmin(ctx sdk.Context, contractAddress, caller, newAdmin sdk.AccAddress, authZ AuthorizationPolicy) error + pinCode(ctx sdk.Context, codeID uint64) error + unpinCode(ctx sdk.Context, codeID uint64) error + execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) + Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error) + setContractInfoExtension(ctx sdk.Context, contract sdk.AccAddress, extra types.ContractInfoExtension) error + setAccessConfig(ctx sdk.Context, codeID uint64, caller sdk.AccAddress, newConfig types.AccessConfig, autz AuthorizationPolicy) error + ClassicAddressGenerator() AddressGenerator +} + +type PermissionedKeeper struct { + authZPolicy AuthorizationPolicy + nested decoratedKeeper +} + +func NewPermissionedKeeper(nested decoratedKeeper, authZPolicy AuthorizationPolicy) *PermissionedKeeper { + return &PermissionedKeeper{authZPolicy: authZPolicy, nested: nested} +} + +func NewGovPermissionKeeper(nested decoratedKeeper) *PermissionedKeeper { + return NewPermissionedKeeper(nested, GovAuthorizationPolicy{}) +} + +func NewDefaultPermissionKeeper(nested decoratedKeeper) *PermissionedKeeper { + return NewPermissionedKeeper(nested, DefaultAuthorizationPolicy{}) +} + +func (p PermissionedKeeper) Create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, instantiateAccess *types.AccessConfig) (codeID uint64, checksum []byte, err error) { + return p.nested.create(ctx, creator, wasmCode, instantiateAccess, p.authZPolicy) +} + +// Instantiate creates an instance of a WASM contract using the classic sequence based address generator +func (p PermissionedKeeper) Instantiate( + ctx sdk.Context, + codeID uint64, + creator, admin sdk.AccAddress, + initMsg []byte, + label string, + deposit sdk.Coins, +) (sdk.AccAddress, []byte, error) { + return p.nested.instantiate(ctx, codeID, creator, admin, initMsg, label, deposit, p.nested.ClassicAddressGenerator(), p.authZPolicy) +} + +// Instantiate2 creates an instance of a WASM contract using the predictable address generator +func (p PermissionedKeeper) Instantiate2( + ctx sdk.Context, + codeID uint64, + creator, admin sdk.AccAddress, + initMsg []byte, + label string, + deposit sdk.Coins, + salt []byte, + fixMsg bool, +) (sdk.AccAddress, []byte, error) { + return p.nested.instantiate( + ctx, + codeID, + creator, + admin, + initMsg, + label, + deposit, + PredicableAddressGenerator(creator, salt, initMsg, fixMsg), + p.authZPolicy, + ) +} + +func (p PermissionedKeeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) { + return p.nested.execute(ctx, contractAddress, caller, msg, coins) +} + +func (p PermissionedKeeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) ([]byte, error) { + return p.nested.migrate(ctx, contractAddress, caller, newCodeID, msg, p.authZPolicy) +} + +func (p PermissionedKeeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error) { + return p.nested.Sudo(ctx, contractAddress, msg) +} + +func (p PermissionedKeeper) UpdateContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newAdmin sdk.AccAddress) error { + return p.nested.setContractAdmin(ctx, contractAddress, caller, newAdmin, p.authZPolicy) +} + +func (p PermissionedKeeper) ClearContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress) error { + return p.nested.setContractAdmin(ctx, contractAddress, caller, nil, p.authZPolicy) +} + +func (p PermissionedKeeper) PinCode(ctx sdk.Context, codeID uint64) error { + return p.nested.pinCode(ctx, codeID) +} + +func (p PermissionedKeeper) UnpinCode(ctx sdk.Context, codeID uint64) error { + return p.nested.unpinCode(ctx, codeID) +} + +// SetContractInfoExtension updates the extra attributes that can be stored with the contract info +func (p PermissionedKeeper) SetContractInfoExtension(ctx sdk.Context, contract sdk.AccAddress, extra types.ContractInfoExtension) error { + return p.nested.setContractInfoExtension(ctx, contract, extra) +} + +// SetAccessConfig updates the access config of a code id. +func (p PermissionedKeeper) SetAccessConfig(ctx sdk.Context, codeID uint64, caller sdk.AccAddress, newConfig types.AccessConfig) error { + return p.nested.setAccessConfig(ctx, codeID, caller, newConfig, p.authZPolicy) +} diff --git a/x/wasm/keeper/contract_keeper_test.go b/x/wasm/keeper/contract_keeper_test.go new file mode 100644 index 00000000..a146a346 --- /dev/null +++ b/x/wasm/keeper/contract_keeper_test.go @@ -0,0 +1,168 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "math" + "strings" + "testing" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestInstantiate2(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + example := StoreHackatomExampleContract(t, parentCtx, keepers) + otherExample := StoreReflectContract(t, parentCtx, keepers) + mock := &wasmtesting.MockWasmer{} + wasmtesting.MakeInstantiable(mock) + keepers.WasmKeeper.wasmVM = mock // set mock to not fail on contract init message + + verifierAddr := RandomAccountAddress(t) + beneficiaryAddr := RandomAccountAddress(t) + initMsg := mustMarshal(t, HackatomExampleInitMsg{Verifier: verifierAddr, Beneficiary: beneficiaryAddr}) + + otherAddr := keepers.Faucet.NewFundedRandomAccount(parentCtx, sdk.NewInt64Coin("denom", 1_000_000_000)) + + const ( + mySalt = "my salt" + myLabel = "my label" + ) + // create instances for duplicate checks + exampleContract := func(t *testing.T, ctx sdk.Context, fixMsg bool) { + _, _, err := keepers.ContractKeeper.Instantiate2( + ctx, + example.CodeID, + example.CreatorAddr, + nil, + initMsg, + myLabel, + sdk.NewCoins(sdk.NewInt64Coin("denom", 1)), + []byte(mySalt), + fixMsg, + ) + require.NoError(t, err) + } + exampleWithFixMsg := func(t *testing.T, ctx sdk.Context) { + exampleContract(t, ctx, true) + } + exampleWithoutFixMsg := func(t *testing.T, ctx sdk.Context) { + exampleContract(t, ctx, false) + } + specs := map[string]struct { + setup func(t *testing.T, ctx sdk.Context) + codeID uint64 + sender sdk.AccAddress + salt []byte + initMsg json.RawMessage + fixMsg bool + expErr error + }{ + "fix msg - generates different address than without fixMsg": { + setup: exampleWithoutFixMsg, + codeID: example.CodeID, + sender: example.CreatorAddr, + salt: []byte(mySalt), + initMsg: initMsg, + fixMsg: true, + }, + "fix msg - different sender": { + setup: exampleWithFixMsg, + codeID: example.CodeID, + sender: otherAddr, + salt: []byte(mySalt), + initMsg: initMsg, + fixMsg: true, + }, + "fix msg - different code": { + setup: exampleWithFixMsg, + codeID: otherExample.CodeID, + sender: example.CreatorAddr, + salt: []byte(mySalt), + initMsg: []byte(`{}`), + fixMsg: true, + }, + "fix msg - different salt": { + setup: exampleWithFixMsg, + codeID: example.CodeID, + sender: example.CreatorAddr, + salt: []byte("other salt"), + initMsg: initMsg, + fixMsg: true, + }, + "fix msg - different init msg": { + setup: exampleWithFixMsg, + codeID: example.CodeID, + sender: example.CreatorAddr, + salt: []byte(mySalt), + initMsg: mustMarshal(t, HackatomExampleInitMsg{Verifier: otherAddr, Beneficiary: beneficiaryAddr}), + fixMsg: true, + }, + "different sender": { + setup: exampleWithoutFixMsg, + codeID: example.CodeID, + sender: otherAddr, + salt: []byte(mySalt), + initMsg: initMsg, + }, + "different code": { + setup: exampleWithoutFixMsg, + codeID: otherExample.CodeID, + sender: example.CreatorAddr, + salt: []byte(mySalt), + initMsg: []byte(`{}`), + }, + "different salt": { + setup: exampleWithoutFixMsg, + codeID: example.CodeID, + sender: example.CreatorAddr, + salt: []byte(`other salt`), + initMsg: initMsg, + }, + "different init msg - reject same address": { + setup: exampleWithoutFixMsg, + codeID: example.CodeID, + sender: example.CreatorAddr, + salt: []byte(mySalt), + initMsg: mustMarshal(t, HackatomExampleInitMsg{Verifier: otherAddr, Beneficiary: beneficiaryAddr}), + expErr: types.ErrDuplicate, + }, + "fix msg - long msg": { + setup: exampleWithFixMsg, + codeID: example.CodeID, + sender: otherAddr, + salt: []byte(mySalt), + initMsg: []byte(fmt.Sprintf(`{"foo":%q}`, strings.Repeat("b", math.MaxInt16+1))), // too long kills CI + fixMsg: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + spec.setup(t, ctx) + gotAddr, _, gotErr := keepers.ContractKeeper.Instantiate2( + ctx, + spec.codeID, + spec.sender, + nil, + spec.initMsg, + myLabel, + sdk.NewCoins(sdk.NewInt64Coin("denom", 2)), + spec.salt, + spec.fixMsg, + ) + if spec.expErr != nil { + assert.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.NotEmpty(t, gotAddr) + }) + } +} diff --git a/x/wasm/keeper/events.go b/x/wasm/keeper/events.go new file mode 100644 index 00000000..25cf14f9 --- /dev/null +++ b/x/wasm/keeper/events.go @@ -0,0 +1,67 @@ +package keeper + +import ( + "fmt" + "strings" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// newWasmModuleEvent creates with wasm module event for interacting with the given contract. Adds custom attributes +// to this event. +func newWasmModuleEvent(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress) (sdk.Events, error) { + attrs, err := contractSDKEventAttributes(customAttributes, contractAddr) + if err != nil { + return nil, err + } + + // each wasm invocation always returns one sdk.Event + return sdk.Events{sdk.NewEvent(types.WasmModuleEventType, attrs...)}, nil +} + +const eventTypeMinLength = 2 + +// newCustomEvents converts wasmvm events from a contract response to sdk type events +func newCustomEvents(evts wasmvmtypes.Events, contractAddr sdk.AccAddress) (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 := contractSDKEventAttributes(e.Attributes, contractAddr) + if err != nil { + return nil, err + } + events = append(events, sdk.NewEvent(fmt.Sprintf("%s%s", types.CustomContractEventPrefix, typ), attributes...)) + } + return events, nil +} + +// convert and add contract address issuing this event +func contractSDKEventAttributes(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress) ([]sdk.Attribute, error) { + attrs := []sdk.Attribute{sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddr.String())} + // append attributes from wasm to the sdk.Event + for _, l := range customAttributes { + // ensure key and value are non-empty (and trim what is there) + key := strings.TrimSpace(l.Key) + if len(key) == 0 { + return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Empty attribute key. Value: %s", l.Value)) + } + value := strings.TrimSpace(l.Value) + // TODO: check if this is legal in the SDK - if it is, we can remove this check + if len(value) == 0 { + return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Empty attribute value. Key: %s", key)) + } + // and reserve all _* keys for our use (not contract) + if strings.HasPrefix(key, types.AttributeReservedPrefix) { + return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Attribute key starts with reserved prefix %s: '%s'", types.AttributeReservedPrefix, key)) + } + attrs = append(attrs, sdk.NewAttribute(key, value)) + } + return attrs, nil +} diff --git a/x/wasm/keeper/events_test.go b/x/wasm/keeper/events_test.go new file mode 100644 index 00000000..0e6de87b --- /dev/null +++ b/x/wasm/keeper/events_test.go @@ -0,0 +1,290 @@ +package keeper + +import ( + "context" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestHasWasmModuleEvent(t *testing.T) { + myContractAddr := RandomAccountAddress(t) + specs := map[string]struct { + srcEvents []sdk.Event + exp bool + }{ + "event found": { + srcEvents: []sdk.Event{ + sdk.NewEvent(types.WasmModuleEventType, sdk.NewAttribute("_contract_address", myContractAddr.String())), + }, + exp: true, + }, + "different event: not found": { + srcEvents: []sdk.Event{ + sdk.NewEvent(types.CustomContractEventPrefix, sdk.NewAttribute("_contract_address", myContractAddr.String())), + }, + exp: false, + }, + "event with different address: not found": { + srcEvents: []sdk.Event{ + sdk.NewEvent(types.WasmModuleEventType, sdk.NewAttribute("_contract_address", RandomBech32AccountAddress(t))), + }, + exp: false, + }, + "no event": { + srcEvents: []sdk.Event{}, + exp: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + em := sdk.NewEventManager() + em.EmitEvents(spec.srcEvents) + ctx := sdk.Context{}.WithContext(context.Background()).WithEventManager(em) + + got := hasWasmModuleEvent(ctx, myContractAddr) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestNewCustomEvents(t *testing.T) { + myContract := RandomAccountAddress(t) + 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-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + 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-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + 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-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("myKey", "myVal")), + sdk.NewEvent("wasm-bar", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("otherKey", "otherVal")), + }, + }, + "without attributes": { + src: wasmvmtypes.Events{{ + Type: "foo", + }}, + exp: sdk.Events{sdk.NewEvent("wasm-foo", + sdk.NewAttribute("_contract_address", myContract.String()))}, + }, + "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-food", + sdk.NewAttribute("_contract_address", myContract.String()), + 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 := newCustomEvents(spec.src, myContract) + if spec.isError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, spec.exp, gotEvent) + } + }) + } +} + +func TestNewWasmModuleEvent(t *testing.T) { + myContract := RandomAccountAddress(t) + 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", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("myKey", "myVal"))}, + }, + "multiple attributes": { + src: []wasmvmtypes.EventAttribute{ + {Key: "myKey", Value: "myVal"}, + {Key: "myOtherKey", Value: "myOtherVal"}, + }, + exp: sdk.Events{sdk.NewEvent("wasm", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("myKey", "myVal"), + sdk.NewAttribute("myOtherKey", "myOtherVal"))}, + }, + "without attributes": { + exp: sdk.Events{sdk.NewEvent("wasm", + sdk.NewAttribute("_contract_address", myContract.String()))}, + }, + "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", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("my-real-key", "some-val"))}, + }, + "empty elements": { + src: make([]wasmvmtypes.EventAttribute, 10), + isError: true, + }, + "nil": { + exp: sdk.Events{sdk.NewEvent("wasm", + sdk.NewAttribute("_contract_address", myContract.String()), + )}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotEvent, err := newWasmModuleEvent(spec.src, myContract) + if spec.isError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, spec.exp, gotEvent) + } + }) + } +} + +// returns true when a wasm module event was emitted for this contract already +func hasWasmModuleEvent(ctx sdk.Context, contractAddr sdk.AccAddress) bool { + for _, e := range ctx.EventManager().Events() { + if e.Type == types.WasmModuleEventType { + for _, a := range e.Attributes { + if string(a.Key) == types.AttributeKeyContractAddr && string(a.Value) == contractAddr.String() { + return true + } + } + } + } + return false +} diff --git a/x/wasm/keeper/gas_register.go b/x/wasm/keeper/gas_register.go new file mode 100644 index 00000000..8ebbd180 --- /dev/null +++ b/x/wasm/keeper/gas_register.go @@ -0,0 +1,252 @@ +package keeper + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +const ( + // DefaultGasMultiplier is how many CosmWasm gas points = 1 Cosmos SDK gas point. + // + // CosmWasm gas strategy is documented in https://github.com/CosmWasm/cosmwasm/blob/v1.0.0-beta/docs/GAS.md. + // Cosmos SDK reference costs can be found here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.10/store/types/gas.go#L198-L209. + // + // The original multiplier of 100 up to CosmWasm 0.16 was based on + // "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)" + // as well as manual Wasmer benchmarks from 2019. This was then multiplied by 150_000 + // in the 0.16 -> 1.0 upgrade (https://github.com/CosmWasm/cosmwasm/pull/1120). + // + // The multiplier deserves more reproducible benchmarking and a strategy that allows easy adjustments. + // This is tracked in https://github.com/CosmWasm/wasmd/issues/566 and https://github.com/CosmWasm/wasmd/issues/631. + // Gas adjustments are consensus breaking but may happen in any release marked as consensus breaking. + // Do not make assumptions on how much gas an operation will consume in places that are hard to adjust, + // such as hardcoding them in contracts. + // + // Please note that all gas prices returned to wasmvm should have this multiplied. + // Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938055852 + DefaultGasMultiplier uint64 = 140_000_000 + // DefaultInstanceCost is how much SDK gas we charge each time we load a WASM instance. + // Creating a new instance is costly, and this helps put a recursion limit to contracts calling contracts. + // Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803 + DefaultInstanceCost uint64 = 60_000 + // DefaultCompileCost is how much SDK gas is charged *per byte* for compiling WASM code. + // Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803 + DefaultCompileCost uint64 = 3 + // DefaultEventAttributeDataCost is how much SDK gas is charged *per byte* for attribute data in events. + // This is used with len(key) + len(value) + DefaultEventAttributeDataCost uint64 = 1 + // DefaultContractMessageDataCost is how much SDK gas is charged *per byte* of the message that goes to the contract + // This is used with len(msg). Note that the message is deserialized in the receiving contract and this is charged + // with wasm gas already. The derserialization of results is also charged in wasmvm. I am unsure if we need to add + // additional costs here. + // Note: also used for error fields on reply, and data on reply. Maybe these should be pulled out to a different (non-zero) field + DefaultContractMessageDataCost uint64 = 0 + // DefaultPerAttributeCost is how much SDK gas we charge per attribute count. + DefaultPerAttributeCost uint64 = 10 + // DefaultPerCustomEventCost is how much SDK gas we charge per event count. + DefaultPerCustomEventCost uint64 = 20 + // DefaultEventAttributeDataFreeTier number of bytes of total attribute data we do not charge. + DefaultEventAttributeDataFreeTier = 100 +) + +// default: 0.15 gas. +// see https://github.com/CosmWasm/wasmd/pull/898#discussion_r937727200 +var defaultPerByteUncompressCost = wasmvmtypes.UFraction{ + Numerator: 15, + Denominator: 100, +} + +// DefaultPerByteUncompressCost is how much SDK gas we charge per source byte to unpack +func DefaultPerByteUncompressCost() wasmvmtypes.UFraction { + return defaultPerByteUncompressCost +} + +// GasRegister abstract source for gas costs +type GasRegister interface { + // NewContractInstanceCosts costs to crate a new contract instance from code + NewContractInstanceCosts(pinned bool, msgLen int) sdk.Gas + // CompileCosts costs to persist and "compile" a new wasm contract + CompileCosts(byteLength int) sdk.Gas + // UncompressCosts costs to unpack a new wasm contract + UncompressCosts(byteLength int) sdk.Gas + // InstantiateContractCosts costs when interacting with a wasm contract + InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas + // ReplyCosts costs to to handle a message reply + ReplyCosts(pinned bool, reply wasmvmtypes.Reply) sdk.Gas + // EventCosts costs to persist an event + EventCosts(attrs []wasmvmtypes.EventAttribute, events wasmvmtypes.Events) sdk.Gas + // ToWasmVMGas converts from sdk gas to wasmvm gas + ToWasmVMGas(source sdk.Gas) uint64 + // FromWasmVMGas converts from wasmvm gas to sdk gas + FromWasmVMGas(source uint64) sdk.Gas +} + +// WasmGasRegisterConfig config type +type WasmGasRegisterConfig struct { + // InstanceCost costs when interacting with a wasm contract + InstanceCost sdk.Gas + // CompileCosts costs to persist and "compile" a new wasm contract + CompileCost sdk.Gas + // UncompressCost costs per byte to unpack a contract + UncompressCost wasmvmtypes.UFraction + // 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 + GasMultiplier sdk.Gas + // EventPerAttributeCost is how much SDK gas is charged *per byte* for attribute data in events. + // This is used with len(key) + len(value) + EventPerAttributeCost sdk.Gas + // EventAttributeDataCost is how much SDK gas is charged *per byte* for attribute data in events. + // This is used with len(key) + len(value) + EventAttributeDataCost sdk.Gas + // EventAttributeDataFreeTier number of bytes of total attribute data that is free of charge + EventAttributeDataFreeTier uint64 + // ContractMessageDataCost SDK gas charged *per byte* of the message that goes to the contract + // This is used with len(msg) + ContractMessageDataCost sdk.Gas + // CustomEventCost cost per custom event + CustomEventCost uint64 +} + +// DefaultGasRegisterConfig default values +func DefaultGasRegisterConfig() WasmGasRegisterConfig { + return WasmGasRegisterConfig{ + InstanceCost: DefaultInstanceCost, + CompileCost: DefaultCompileCost, + GasMultiplier: DefaultGasMultiplier, + EventPerAttributeCost: DefaultPerAttributeCost, + CustomEventCost: DefaultPerCustomEventCost, + EventAttributeDataCost: DefaultEventAttributeDataCost, + EventAttributeDataFreeTier: DefaultEventAttributeDataFreeTier, + ContractMessageDataCost: DefaultContractMessageDataCost, + UncompressCost: DefaultPerByteUncompressCost(), + } +} + +// WasmGasRegister implements GasRegister interface +type WasmGasRegister struct { + c WasmGasRegisterConfig +} + +// NewDefaultWasmGasRegister creates instance with default values +func NewDefaultWasmGasRegister() WasmGasRegister { + return NewWasmGasRegister(DefaultGasRegisterConfig()) +} + +// NewWasmGasRegister constructor +func NewWasmGasRegister(c WasmGasRegisterConfig) WasmGasRegister { + if c.GasMultiplier == 0 { + panic(sdkerrors.Wrap(sdkerrors.ErrLogic, "GasMultiplier can not be 0")) + } + return WasmGasRegister{ + c: c, + } +} + +// NewContractInstanceCosts costs to crate a new contract instance from code +func (g WasmGasRegister) NewContractInstanceCosts(pinned bool, msgLen int) storetypes.Gas { + return g.InstantiateContractCosts(pinned, msgLen) +} + +// CompileCosts costs to persist and "compile" a new wasm contract +func (g WasmGasRegister) CompileCosts(byteLength int) storetypes.Gas { + if byteLength < 0 { + panic(sdkerrors.Wrap(types.ErrInvalid, "negative length")) + } + return g.c.CompileCost * uint64(byteLength) +} + +// UncompressCosts costs to unpack a new wasm contract +func (g WasmGasRegister) UncompressCosts(byteLength int) sdk.Gas { + if byteLength < 0 { + panic(sdkerrors.Wrap(types.ErrInvalid, "negative length")) + } + return g.c.UncompressCost.Mul(uint64(byteLength)).Floor() +} + +// InstantiateContractCosts costs when interacting with a wasm contract +func (g WasmGasRegister) InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas { + if msgLen < 0 { + panic(sdkerrors.Wrap(types.ErrInvalid, "negative length")) + } + dataCosts := sdk.Gas(msgLen) * g.c.ContractMessageDataCost + if pinned { + return dataCosts + } + return g.c.InstanceCost + dataCosts +} + +// ReplyCosts costs to to handle a message reply +func (g WasmGasRegister) ReplyCosts(pinned bool, reply wasmvmtypes.Reply) sdk.Gas { + var eventGas sdk.Gas + msgLen := len(reply.Result.Err) + if reply.Result.Ok != nil { + msgLen += len(reply.Result.Ok.Data) + var attrs []wasmvmtypes.EventAttribute + for _, e := range reply.Result.Ok.Events { + eventGas += sdk.Gas(len(e.Type)) * g.c.EventAttributeDataCost + attrs = append(attrs, e.Attributes...) + } + // apply free tier on the whole set not per event + eventGas += g.EventCosts(attrs, nil) + } + return eventGas + g.InstantiateContractCosts(pinned, msgLen) +} + +// EventCosts costs to persist an event +func (g WasmGasRegister) EventCosts(attrs []wasmvmtypes.EventAttribute, events wasmvmtypes.Events) sdk.Gas { + gas, remainingFreeTier := g.eventAttributeCosts(attrs, g.c.EventAttributeDataFreeTier) + for _, e := range events { + gas += g.c.CustomEventCost + gas += sdk.Gas(len(e.Type)) * g.c.EventAttributeDataCost // no free tier with event type + var attrCost sdk.Gas + attrCost, remainingFreeTier = g.eventAttributeCosts(e.Attributes, remainingFreeTier) + gas += attrCost + } + return gas +} + +func (g WasmGasRegister) eventAttributeCosts(attrs []wasmvmtypes.EventAttribute, freeTier uint64) (sdk.Gas, uint64) { + if len(attrs) == 0 { + return 0, freeTier + } + var storedBytes uint64 + for _, l := range attrs { + storedBytes += uint64(len(l.Key)) + uint64(len(l.Value)) + } + storedBytes, freeTier = calcWithFreeTier(storedBytes, freeTier) + // total Length * costs + attribute count * costs + r := sdk.NewIntFromUint64(g.c.EventAttributeDataCost).Mul(sdk.NewIntFromUint64(storedBytes)). + Add(sdk.NewIntFromUint64(g.c.EventPerAttributeCost).Mul(sdk.NewIntFromUint64(uint64(len(attrs))))) + if !r.IsUint64() { + panic(sdk.ErrorOutOfGas{Descriptor: "overflow"}) + } + return r.Uint64(), freeTier +} + +// apply free tier +func calcWithFreeTier(storedBytes uint64, freeTier uint64) (uint64, uint64) { + if storedBytes <= freeTier { + return 0, freeTier - storedBytes + } + storedBytes -= freeTier + return storedBytes, 0 +} + +// ToWasmVMGas convert to wasmVM contract runtime gas unit +func (g WasmGasRegister) ToWasmVMGas(source storetypes.Gas) uint64 { + x := source * g.c.GasMultiplier + if x < source { + panic(sdk.ErrorOutOfGas{Descriptor: "overflow"}) + } + return x +} + +// FromWasmVMGas converts to SDK gas unit +func (g WasmGasRegister) FromWasmVMGas(source uint64) sdk.Gas { + return source / g.c.GasMultiplier +} diff --git a/x/wasm/keeper/gas_register_test.go b/x/wasm/keeper/gas_register_test.go new file mode 100644 index 00000000..f6d4b12e --- /dev/null +++ b/x/wasm/keeper/gas_register_test.go @@ -0,0 +1,472 @@ +package keeper + +import ( + "math" + "strings" + "testing" + + "github.com/cerc-io/laconicd/x/wasm/types" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" +) + +func TestCompileCosts(t *testing.T) { + specs := map[string]struct { + srcLen int + srcConfig WasmGasRegisterConfig + exp sdk.Gas + expPanic bool + }{ + "one byte": { + srcLen: 1, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(3), // DefaultCompileCost + }, + "zero byte": { + srcLen: 0, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(0), + }, + "negative len": { + srcLen: -1, + srcConfig: DefaultGasRegisterConfig(), + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { + NewWasmGasRegister(spec.srcConfig).CompileCosts(spec.srcLen) + }) + return + } + gotGas := NewWasmGasRegister(spec.srcConfig).CompileCosts(spec.srcLen) + assert.Equal(t, spec.exp, gotGas) + }) + } +} + +func TestNewContractInstanceCosts(t *testing.T) { + specs := map[string]struct { + srcLen int + srcConfig WasmGasRegisterConfig + pinned bool + exp sdk.Gas + expPanic bool + }{ + "small msg - pinned": { + srcLen: 1, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: DefaultContractMessageDataCost, + }, + "big msg - pinned": { + srcLen: math.MaxUint32, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: DefaultContractMessageDataCost * sdk.Gas(math.MaxUint32), + }, + "empty msg - pinned": { + srcLen: 0, + pinned: true, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(0), + }, + "small msg - unpinned": { + srcLen: 1, + srcConfig: DefaultGasRegisterConfig(), + exp: DefaultContractMessageDataCost + DefaultInstanceCost, + }, + "big msg - unpinned": { + srcLen: math.MaxUint32, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultContractMessageDataCost*math.MaxUint32 + DefaultInstanceCost), + }, + "empty msg - unpinned": { + srcLen: 0, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultInstanceCost), + }, + + "negative len": { + srcLen: -1, + srcConfig: DefaultGasRegisterConfig(), + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { + NewWasmGasRegister(spec.srcConfig).NewContractInstanceCosts(spec.pinned, spec.srcLen) + }) + return + } + gotGas := NewWasmGasRegister(spec.srcConfig).NewContractInstanceCosts(spec.pinned, spec.srcLen) + assert.Equal(t, spec.exp, gotGas) + }) + } +} + +func TestContractInstanceCosts(t *testing.T) { + // same as TestNewContractInstanceCosts currently + specs := map[string]struct { + srcLen int + srcConfig WasmGasRegisterConfig + pinned bool + exp sdk.Gas + expPanic bool + }{ + "small msg - pinned": { + srcLen: 1, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: DefaultContractMessageDataCost, + }, + "big msg - pinned": { + srcLen: math.MaxUint32, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: sdk.Gas(DefaultContractMessageDataCost * math.MaxUint32), + }, + "empty msg - pinned": { + srcLen: 0, + pinned: true, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(0), + }, + "small msg - unpinned": { + srcLen: 1, + srcConfig: DefaultGasRegisterConfig(), + exp: DefaultContractMessageDataCost + DefaultInstanceCost, + }, + "big msg - unpinned": { + srcLen: math.MaxUint32, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultContractMessageDataCost*math.MaxUint32 + DefaultInstanceCost), + }, + "empty msg - unpinned": { + srcLen: 0, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultInstanceCost), + }, + + "negative len": { + srcLen: -1, + srcConfig: DefaultGasRegisterConfig(), + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { + NewWasmGasRegister(spec.srcConfig).InstantiateContractCosts(spec.pinned, spec.srcLen) + }) + return + } + gotGas := NewWasmGasRegister(spec.srcConfig).InstantiateContractCosts(spec.pinned, spec.srcLen) + assert.Equal(t, spec.exp, gotGas) + }) + } +} + +func TestReplyCost(t *testing.T) { + specs := map[string]struct { + src wasmvmtypes.Reply + srcConfig WasmGasRegisterConfig + pinned bool + exp sdk.Gas + expPanic bool + }{ + "subcall response with events and data - pinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: []wasmvmtypes.Event{ + {Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}}, + }, + Data: []byte{0x1}, + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: sdk.Gas(3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost), // 3 == len("foo") + }, + "subcall response with events - pinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: []wasmvmtypes.Event{ + {Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}}, + }, + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: sdk.Gas(3*DefaultEventAttributeDataCost + DefaultPerAttributeCost), // 3 == len("foo") + }, + "subcall response with events exceeds free tier- pinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: []wasmvmtypes.Event{ + {Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: strings.Repeat("x", DefaultEventAttributeDataFreeTier), Value: "myData"}}}, + }, + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: sdk.Gas((3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost), // 3 == len("foo"), 6 == len("myData") + }, + "subcall response error - pinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Err: "foo", + }, + }, + srcConfig: DefaultGasRegisterConfig(), + pinned: true, + exp: 3 * DefaultContractMessageDataCost, + }, + "subcall response with events and data - unpinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: []wasmvmtypes.Event{ + {Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}}, + }, + Data: []byte{0x1}, + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultInstanceCost + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost), + }, + "subcall response with events - unpinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: []wasmvmtypes.Event{ + {Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myData"}}}, + }, + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultInstanceCost + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost), + }, + "subcall response with events exceeds free tier- unpinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: []wasmvmtypes.Event{ + {Type: "foo", Attributes: []wasmvmtypes.EventAttribute{{Key: strings.Repeat("x", DefaultEventAttributeDataFreeTier), Value: "myData"}}}, + }, + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultInstanceCost + (3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost), // 3 == len("foo"), 6 == len("myData") + }, + "subcall response error - unpinned": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Err: "foo", + }, + }, + srcConfig: DefaultGasRegisterConfig(), + exp: sdk.Gas(DefaultInstanceCost + 3*DefaultContractMessageDataCost), + }, + "subcall response with empty events": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: make([]wasmvmtypes.Event, 10), + }, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + exp: DefaultInstanceCost, + }, + "subcall response with events unset": { + src: wasmvmtypes.Reply{ + Result: wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{}, + }, + }, + srcConfig: DefaultGasRegisterConfig(), + exp: DefaultInstanceCost, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { + NewWasmGasRegister(spec.srcConfig).ReplyCosts(spec.pinned, spec.src) + }) + return + } + gotGas := NewWasmGasRegister(spec.srcConfig).ReplyCosts(spec.pinned, spec.src) + assert.Equal(t, spec.exp, gotGas) + }) + } +} + +func TestEventCosts(t *testing.T) { + // most cases are covered in TestReplyCost already. This ensures some edge cases + specs := map[string]struct { + srcAttrs []wasmvmtypes.EventAttribute + srcEvents wasmvmtypes.Events + expGas sdk.Gas + }{ + "empty events": { + srcEvents: make([]wasmvmtypes.Event, 1), + expGas: DefaultPerCustomEventCost, + }, + "empty attributes": { + srcAttrs: make([]wasmvmtypes.EventAttribute, 1), + expGas: DefaultPerAttributeCost, + }, + "both nil": { + expGas: 0, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotGas := NewDefaultWasmGasRegister().EventCosts(spec.srcAttrs, spec.srcEvents) + assert.Equal(t, spec.expGas, gotGas) + }) + } +} + +func TestToWasmVMGasConversion(t *testing.T) { + specs := map[string]struct { + src storetypes.Gas + srcConfig WasmGasRegisterConfig + exp uint64 + expPanic bool + }{ + "0": { + src: 0, + exp: 0, + srcConfig: DefaultGasRegisterConfig(), + }, + "max": { + srcConfig: WasmGasRegisterConfig{ + GasMultiplier: 1, + }, + src: math.MaxUint64, + exp: math.MaxUint64, + }, + "overflow": { + srcConfig: WasmGasRegisterConfig{ + GasMultiplier: 2, + }, + src: math.MaxUint64, + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { + r := NewWasmGasRegister(spec.srcConfig) + _ = r.ToWasmVMGas(spec.src) + }) + return + } + r := NewWasmGasRegister(spec.srcConfig) + got := r.ToWasmVMGas(spec.src) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestFromWasmVMGasConversion(t *testing.T) { + specs := map[string]struct { + src uint64 + exp storetypes.Gas + srcConfig WasmGasRegisterConfig + expPanic bool + }{ + "0": { + src: 0, + exp: 0, + srcConfig: DefaultGasRegisterConfig(), + }, + "max": { + srcConfig: WasmGasRegisterConfig{ + GasMultiplier: 1, + }, + src: math.MaxUint64, + exp: math.MaxUint64, + }, + "missconfigured": { + srcConfig: WasmGasRegisterConfig{ + GasMultiplier: 0, + }, + src: 1, + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { + r := NewWasmGasRegister(spec.srcConfig) + _ = r.FromWasmVMGas(spec.src) + }) + return + } + r := NewWasmGasRegister(spec.srcConfig) + got := r.FromWasmVMGas(spec.src) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestUncompressCosts(t *testing.T) { + specs := map[string]struct { + lenIn int + exp sdk.Gas + expPanic bool + }{ + "0": { + exp: 0, + }, + "even": { + lenIn: 100, + exp: 15, + }, + "round down when uneven": { + lenIn: 19, + exp: 2, + }, + "max len": { + lenIn: types.MaxWasmSize, + exp: 122880, + }, + "invalid len": { + lenIn: -1, + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { NewDefaultWasmGasRegister().UncompressCosts(spec.lenIn) }) + return + } + got := NewDefaultWasmGasRegister().UncompressCosts(spec.lenIn) + assert.Equal(t, spec.exp, got) + }) + } +} diff --git a/x/wasm/keeper/genesis.go b/x/wasm/keeper/genesis.go new file mode 100644 index 00000000..84a113ef --- /dev/null +++ b/x/wasm/keeper/genesis.go @@ -0,0 +1,116 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// ValidatorSetSource is a subset of the staking keeper +type ValidatorSetSource interface { + ApplyAndReturnValidatorSetUpdates(sdk.Context) (updates []abci.ValidatorUpdate, err error) +} + +// 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 types.GenesisState) ([]abci.ValidatorUpdate, error) { + contractKeeper := NewGovPermissionKeeper(keeper) + keeper.SetParams(ctx, data.Params) + var maxCodeID uint64 + for i, code := range data.Codes { + err := keeper.importCode(ctx, code.CodeID, code.CodeInfo, code.CodeBytes) + if err != nil { + return nil, sdkerrors.Wrapf(err, "code %d with id: %d", i, code.CodeID) + } + if code.CodeID > maxCodeID { + maxCodeID = code.CodeID + } + if code.Pinned { + if err := contractKeeper.PinCode(ctx, code.CodeID); err != nil { + return nil, sdkerrors.Wrapf(err, "contract number %d", i) + } + } + } + + var maxContractID int + for i, contract := range data.Contracts { + contractAddr, err := sdk.AccAddressFromBech32(contract.ContractAddress) + if err != nil { + return nil, sdkerrors.Wrapf(err, "address in contract number %d", i) + } + err = keeper.importContract(ctx, contractAddr, &contract.ContractInfo, contract.ContractState, contract.ContractCodeHistory) + if err != nil { + return nil, sdkerrors.Wrapf(err, "contract number %d", i) + } + maxContractID = i + 1 // not ideal but max(contractID) is not persisted otherwise + } + + for i, seq := range data.Sequences { + err := keeper.importAutoIncrementID(ctx, seq.IDKey, seq.Value) + if err != nil { + return nil, sdkerrors.Wrapf(err, "sequence number %d", i) + } + } + + // sanity check seq values + seqVal := keeper.PeekAutoIncrementID(ctx, types.KeyLastCodeID) + if seqVal <= maxCodeID { + return nil, sdkerrors.Wrapf(types.ErrInvalid, "seq %s with value: %d must be greater than: %d ", string(types.KeyLastCodeID), seqVal, maxCodeID) + } + seqVal = keeper.PeekAutoIncrementID(ctx, types.KeyLastInstanceID) + if seqVal <= uint64(maxContractID) { + return nil, sdkerrors.Wrapf(types.ErrInvalid, "seq %s with value: %d must be greater than: %d ", string(types.KeyLastInstanceID), seqVal, maxContractID) + } + return nil, nil +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func ExportGenesis(ctx sdk.Context, keeper *Keeper) *types.GenesisState { + var genState types.GenesisState + + genState.Params = keeper.GetParams(ctx) + + keeper.IterateCodeInfos(ctx, func(codeID uint64, info types.CodeInfo) bool { + bytecode, err := keeper.GetByteCode(ctx, codeID) + if err != nil { + panic(err) + } + genState.Codes = append(genState.Codes, types.Code{ + CodeID: codeID, + CodeInfo: info, + CodeBytes: bytecode, + Pinned: keeper.IsPinnedCode(ctx, codeID), + }) + return false + }) + + keeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, contract types.ContractInfo) bool { + var state []types.Model + keeper.IterateContractState(ctx, addr, func(key, value []byte) bool { + state = append(state, types.Model{Key: key, Value: value}) + return false + }) + + contractCodeHistory := keeper.GetContractHistory(ctx, addr) + + genState.Contracts = append(genState.Contracts, types.Contract{ + ContractAddress: addr.String(), + ContractInfo: contract, + ContractState: state, + ContractCodeHistory: contractCodeHistory, + }) + return false + }) + + for _, k := range [][]byte{types.KeyLastCodeID, types.KeyLastInstanceID} { + genState.Sequences = append(genState.Sequences, types.Sequence{ + IDKey: k, + Value: keeper.PeekAutoIncrementID(ctx, k), + }) + } + + return &genState +} diff --git a/x/wasm/keeper/genesis_test.go b/x/wasm/keeper/genesis_test.go new file mode 100644 index 00000000..7d9ecab6 --- /dev/null +++ b/x/wasm/keeper/genesis_test.go @@ -0,0 +1,671 @@ +package keeper + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "math/rand" + "os" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "github.com/cerc-io/laconicd/x/wasm/types" + wasmTypes "github.com/cerc-io/laconicd/x/wasm/types" +) + +const firstCodeID = 1 + +func TestGenesisExportImport(t *testing.T) { + wasmKeeper, srcCtx, srcStoreKeys := setupKeeper(t) + contractKeeper := NewGovPermissionKeeper(wasmKeeper) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + // store some test data + f := fuzz.New().Funcs(ModelFuzzers...) + + wasmKeeper.SetParams(srcCtx, types.DefaultParams()) + + for i := 0; i < 25; i++ { + var ( + codeInfo types.CodeInfo + contract types.ContractInfo + stateModels []types.Model + history []types.ContractCodeHistoryEntry + pinned bool + contractExtension bool + ) + f.Fuzz(&codeInfo) + f.Fuzz(&contract) + f.Fuzz(&stateModels) + f.NilChance(0).Fuzz(&history) + f.Fuzz(&pinned) + f.Fuzz(&contractExtension) + + creatorAddr, err := sdk.AccAddressFromBech32(codeInfo.Creator) + require.NoError(t, err) + codeID, _, err := contractKeeper.Create(srcCtx, creatorAddr, wasmCode, &codeInfo.InstantiateConfig) + require.NoError(t, err) + if pinned { + contractKeeper.PinCode(srcCtx, codeID) + } + if contractExtension { + anyTime := time.Now().UTC() + var nestedType govtypes.TextProposal + f.NilChance(0).Fuzz(&nestedType) + myExtension, err := govtypes.NewProposal(&nestedType, 1, anyTime, anyTime) + require.NoError(t, err) + contract.SetExtension(&myExtension) + } + + contract.CodeID = codeID + contractAddr := wasmKeeper.ClassicAddressGenerator()(srcCtx, codeID, nil) + wasmKeeper.storeContractInfo(srcCtx, contractAddr, &contract) + wasmKeeper.appendToContractHistory(srcCtx, contractAddr, history...) + wasmKeeper.importContractState(srcCtx, contractAddr, stateModels) + } + var wasmParams types.Params + f.NilChance(0).Fuzz(&wasmParams) + wasmKeeper.SetParams(srcCtx, wasmParams) + + // export + exportedState := ExportGenesis(srcCtx, wasmKeeper) + // order should not matter + rand.Shuffle(len(exportedState.Codes), func(i, j int) { + exportedState.Codes[i], exportedState.Codes[j] = exportedState.Codes[j], exportedState.Codes[i] + }) + rand.Shuffle(len(exportedState.Contracts), func(i, j int) { + exportedState.Contracts[i], exportedState.Contracts[j] = exportedState.Contracts[j], exportedState.Contracts[i] + }) + rand.Shuffle(len(exportedState.Sequences), func(i, j int) { + exportedState.Sequences[i], exportedState.Sequences[j] = exportedState.Sequences[j], exportedState.Sequences[i] + }) + exportedGenesis, err := wasmKeeper.cdc.MarshalJSON(exportedState) + require.NoError(t, err) + + // setup new instances + dstKeeper, dstCtx, dstStoreKeys := setupKeeper(t) + + // reset contract code index in source DB for comparison with dest DB + wasmKeeper.IterateContractInfo(srcCtx, func(address sdk.AccAddress, info wasmTypes.ContractInfo) bool { + creatorAddress := sdk.MustAccAddressFromBech32(info.Creator) + history := wasmKeeper.GetContractHistory(srcCtx, address) + + wasmKeeper.addToContractCodeSecondaryIndex(srcCtx, address, history[len(history)-1]) + wasmKeeper.addToContractCreatorSecondaryIndex(srcCtx, creatorAddress, history[0].Updated, address) + return false + }) + + // re-import + var importState wasmTypes.GenesisState + err = dstKeeper.cdc.UnmarshalJSON(exportedGenesis, &importState) + require.NoError(t, err) + InitGenesis(dstCtx, dstKeeper, importState) + + // compare whole DB + for j := range srcStoreKeys { + srcIT := srcCtx.KVStore(srcStoreKeys[j]).Iterator(nil, nil) + dstIT := dstCtx.KVStore(dstStoreKeys[j]).Iterator(nil, nil) + + for i := 0; srcIT.Valid(); i++ { + require.True(t, dstIT.Valid(), "[%s] destination DB has less elements than source. Missing: %x", srcStoreKeys[j].Name(), srcIT.Key()) + require.Equal(t, srcIT.Key(), dstIT.Key(), i) + require.Equal(t, srcIT.Value(), dstIT.Value(), "[%s] element (%d): %X", srcStoreKeys[j].Name(), i, srcIT.Key()) + dstIT.Next() + srcIT.Next() + } + if !assert.False(t, dstIT.Valid()) { + t.Fatalf("dest Iterator still has key :%X", dstIT.Key()) + } + srcIT.Close() + dstIT.Close() + } +} + +func TestGenesisInit(t *testing.T) { + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + myCodeInfo := wasmTypes.CodeInfoFixture(wasmTypes.WithSHA256CodeHash(wasmCode)) + specs := map[string]struct { + src types.GenesisState + expSuccess bool + }{ + "happy path: code info correct": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 2}, + {IDKey: types.KeyLastInstanceID, Value: 1}, + }, + Params: types.DefaultParams(), + }, + expSuccess: true, + }, + "happy path: code ids can contain gaps": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }, { + CodeID: 3, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 10}, + {IDKey: types.KeyLastInstanceID, Value: 1}, + }, + Params: types.DefaultParams(), + }, + expSuccess: true, + }, + "happy path: code order does not matter": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: 2, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }, { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Contracts: nil, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 3}, + {IDKey: types.KeyLastInstanceID, Value: 1}, + }, + Params: types.DefaultParams(), + }, + expSuccess: true, + }, + "prevent code hash mismatch": {src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: wasmTypes.CodeInfoFixture(func(i *wasmTypes.CodeInfo) { i.CodeHash = make([]byte, sha256.Size) }), + CodeBytes: wasmCode, + }}, + Params: types.DefaultParams(), + }}, + "prevent duplicate codeIDs": {src: types.GenesisState{ + Codes: []types.Code{ + { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }, + { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }, + }, + Params: types.DefaultParams(), + }}, + "codes with same checksum can be pinned": { + src: types.GenesisState{ + Codes: []types.Code{ + { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + Pinned: true, + }, + { + CodeID: 2, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + Pinned: true, + }, + }, + Params: types.DefaultParams(), + }, + }, + "happy path: code id in info and contract do match": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{}`), + }, + }, + }, + }, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 2}, + {IDKey: types.KeyLastInstanceID, Value: 2}, + }, + Params: types.DefaultParams(), + }, + expSuccess: true, + }, + "happy path: code info with two contracts": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{}`), + }, + }, + }, { + ContractAddress: BuildContractAddressClassic(1, 2).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{"foo":"bar"}`), + }, + }, + }, + }, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 2}, + {IDKey: types.KeyLastInstanceID, Value: 3}, + }, + Params: types.DefaultParams(), + }, + expSuccess: true, + }, + "prevent contracts that points to non existing codeID": { + src: types.GenesisState{ + Contracts: []types.Contract{ + { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{"foo":"bar"}`), + }, + }, + }, + }, + Params: types.DefaultParams(), + }, + }, + "prevent duplicate contract address": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{"foo":"bar"}`), + }, + }, + }, { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{"other":"value"}`), + }, + }, + }, + }, + Params: types.DefaultParams(), + }, + }, + "prevent duplicate contract model keys": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractState: []types.Model{ + { + Key: []byte{0x1}, + Value: []byte("foo"), + }, + { + Key: []byte{0x1}, + Value: []byte("bar"), + }, + }, + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{"foo":"bar"}`), + }, + }, + }, + }, + Params: types.DefaultParams(), + }, + }, + "prevent duplicate sequences": { + src: types.GenesisState{ + Sequences: []types.Sequence{ + {IDKey: []byte("foo"), Value: 1}, + {IDKey: []byte("foo"), Value: 9999}, + }, + Params: types.DefaultParams(), + }, + }, + "prevent code id seq init value == max codeID used": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: 2, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 1}, + }, + Params: types.DefaultParams(), + }, + }, + "prevent contract id seq init value == count contracts": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodeBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: BuildContractAddressClassic(1, 1).String(), + ContractInfo: types.ContractInfoFixture(func(c *wasmTypes.ContractInfo) { c.CodeID = 1 }, types.RandCreatedFields), + ContractCodeHistory: []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 1, + Updated: &types.AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()}, + Msg: []byte(`{}`), + }, + }, + }, + }, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: 2}, + {IDKey: types.KeyLastInstanceID, Value: 1}, + }, + Params: types.DefaultParams(), + }, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + keeper, ctx, _ := setupKeeper(t) + + require.NoError(t, types.ValidateGenesis(spec.src)) + _, gotErr := InitGenesis(ctx, keeper, spec.src) + if !spec.expSuccess { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + + for _, c := range spec.src.Codes { + assert.Equal(t, c.Pinned, keeper.IsPinnedCode(ctx, c.CodeID)) + } + }) + } +} + +func TestImportContractWithCodeHistoryPreserved(t *testing.T) { + genesisTemplate := ` +{ + "params":{ + "code_upload_access": { + "permission": "Everybody" + }, + "instantiate_default_permission": "Everybody" + }, + "codes": [ + { + "code_id": "1", + "code_info": { + "code_hash": %q, + "creator": "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx", + "instantiate_config": { + "permission": "OnlyAddress", + "address": "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx" + } + }, + "code_bytes": %q + } + ], + "contracts": [ + { + "contract_address": "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", + "contract_info": { + "code_id": "1", + "creator": "cosmos13x849jzd03vne42ynpj25hn8npjecxqrjghd8x", + "admin": "cosmos1h5t8zxmjr30e9dqghtlpl40f2zz5cgey6esxtn", + "label": "ȀĴnZV芢毤", + "created": { + "block_height" : "100", + "tx_index" : "10" + } + }, + "contract_code_history": [ + { + "operation": "CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT", + "code_id": "1", + "updated": { + "block_height" : "100", + "tx_index" : "10" + }, + "msg": {"foo": "bar"} + }, + { + "operation": "CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE", + "code_id": "1", + "updated": { + "block_height" : "200", + "tx_index" : "10" + }, + "msg": {"other": "msg"} + } + ] + } + ], + "sequences": [ + {"id_key": "BGxhc3RDb2RlSWQ=", "value": "2"}, + {"id_key": "BGxhc3RDb250cmFjdElk", "value": "3"} + ] +}` + keeper, ctx, _ := setupKeeper(t) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + wasmCodeHash := sha256.Sum256(wasmCode) + enc64 := base64.StdEncoding.EncodeToString + genesisStr := fmt.Sprintf(genesisTemplate, enc64(wasmCodeHash[:]), enc64(wasmCode)) + + var importState wasmTypes.GenesisState + err = keeper.cdc.UnmarshalJSON([]byte(genesisStr), &importState) + require.NoError(t, err) + require.NoError(t, importState.ValidateBasic(), genesisStr) + + ctx = ctx.WithBlockHeight(0).WithGasMeter(sdk.NewInfiniteGasMeter()) + + // when + _, err = InitGenesis(ctx, keeper, importState) + require.NoError(t, err) + + // verify wasm code + gotWasmCode, err := keeper.GetByteCode(ctx, 1) + require.NoError(t, err) + assert.Equal(t, wasmCode, gotWasmCode, "byte code does not match") + + // verify code info + gotCodeInfo := keeper.GetCodeInfo(ctx, 1) + require.NotNil(t, gotCodeInfo) + codeCreatorAddr := "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx" + expCodeInfo := types.CodeInfo{ + CodeHash: wasmCodeHash[:], + Creator: codeCreatorAddr, + InstantiateConfig: wasmTypes.AccessConfig{ + Permission: types.AccessTypeOnlyAddress, + Address: codeCreatorAddr, + }, + } + assert.Equal(t, expCodeInfo, *gotCodeInfo) + + // verify contract + contractAddr, _ := sdk.AccAddressFromBech32("cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr") + gotContractInfo := keeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, gotContractInfo) + contractCreatorAddr := "cosmos13x849jzd03vne42ynpj25hn8npjecxqrjghd8x" + adminAddr := "cosmos1h5t8zxmjr30e9dqghtlpl40f2zz5cgey6esxtn" + + expContractInfo := types.ContractInfo{ + CodeID: firstCodeID, + Creator: contractCreatorAddr, + Admin: adminAddr, + Label: "ȀĴnZV芢毤", + Created: &types.AbsoluteTxPosition{BlockHeight: 100, TxIndex: 10}, + } + assert.Equal(t, expContractInfo, *gotContractInfo) + + expHistory := []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Updated: &types.AbsoluteTxPosition{ + BlockHeight: 100, + TxIndex: 10, + }, + Msg: []byte(`{"foo": "bar"}`), + }, + { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: firstCodeID, + Updated: &types.AbsoluteTxPosition{ + BlockHeight: 200, + TxIndex: 10, + }, + Msg: []byte(`{"other": "msg"}`), + }, + } + assert.Equal(t, expHistory, keeper.GetContractHistory(ctx, contractAddr)) + assert.Equal(t, uint64(2), keeper.PeekAutoIncrementID(ctx, types.KeyLastCodeID)) + assert.Equal(t, uint64(3), keeper.PeekAutoIncrementID(ctx, types.KeyLastInstanceID)) +} + +func setupKeeper(t *testing.T) (*Keeper, sdk.Context, []sdk.StoreKey) { + t.Helper() + tempDir, err := os.MkdirTemp("", "wasm") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tempDir) }) + var ( + keyParams = sdk.NewKVStoreKey(paramtypes.StoreKey) + tkeyParams = sdk.NewTransientStoreKey(paramtypes.TStoreKey) + keyWasm = sdk.NewKVStoreKey(wasmTypes.StoreKey) + ) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyWasm, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + require.NoError(t, ms.LoadLatestVersion()) + + ctx := sdk.NewContext(ms, tmproto.Header{ + Height: 1234567, + Time: time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC), + }, false, log.NewNopLogger()) + + encodingConfig := MakeEncodingConfig(t) + // register an example extension. must be protobuf + encodingConfig.InterfaceRegistry.RegisterImplementations( + (*types.ContractInfoExtension)(nil), + &govtypes.Proposal{}, + ) + // also registering gov interfaces for nested Any type + govtypes.RegisterInterfaces(encodingConfig.InterfaceRegistry) + + wasmConfig := wasmTypes.DefaultWasmConfig() + pk := paramskeeper.NewKeeper(encodingConfig.Marshaler, encodingConfig.Amino, keyParams, tkeyParams) + + srcKeeper := NewKeeper( + encodingConfig.Marshaler, + keyWasm, + pk.Subspace(wasmTypes.ModuleName), + authkeeper.AccountKeeper{}, + &bankkeeper.BaseKeeper{}, + stakingkeeper.Keeper{}, + distributionkeeper.Keeper{}, + nil, + nil, + nil, + nil, + nil, + nil, + tempDir, + wasmConfig, + AvailableCapabilities, + ) + return &srcKeeper, ctx, []sdk.StoreKey{keyWasm, keyParams} +} diff --git a/x/wasm/keeper/handler_plugin.go b/x/wasm/keeper/handler_plugin.go new file mode 100644 index 00000000..14aa4ec5 --- /dev/null +++ b/x/wasm/keeper/handler_plugin.go @@ -0,0 +1,226 @@ +package keeper + +import ( + "errors" + "fmt" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/baseapp" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v4/modules/core/24-host" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// msgEncoder is an extension point to customize encodings +type msgEncoder interface { + // Encode converts wasmvm message to n cosmos message types + Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error) +} + +// MessageRouter ADR 031 request type routing +type MessageRouter interface { + Handler(msg sdk.Msg) baseapp.MsgServiceHandler +} + +// SDKMessageHandler can handles messages that can be encoded into sdk.Message types and routed. +type SDKMessageHandler struct { + router MessageRouter + encoders msgEncoder +} + +func NewDefaultMessageHandler( + router MessageRouter, + channelKeeper types.ChannelKeeper, + capabilityKeeper types.CapabilityKeeper, + bankKeeper types.Burner, + unpacker codectypes.AnyUnpacker, + portSource types.ICS20TransferPortSource, + customEncoders ...*MessageEncoders, +) Messenger { + encoders := DefaultEncoders(unpacker, portSource) + for _, e := range customEncoders { + encoders = encoders.Merge(e) + } + return NewMessageHandlerChain( + NewSDKMessageHandler(router, encoders), + NewIBCRawPacketHandler(channelKeeper, capabilityKeeper), + NewBurnCoinMessageHandler(bankKeeper), + ) +} + +func NewSDKMessageHandler(router MessageRouter, encoders msgEncoder) SDKMessageHandler { + return SDKMessageHandler{ + router: router, + encoders: encoders, + } +} + +func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + sdkMsgs, err := h.encoders.Encode(ctx, contractAddr, contractIBCPortID, msg) + if err != nil { + return nil, nil, err + } + for _, sdkMsg := range sdkMsgs { + res, err := h.handleSdkMessage(ctx, contractAddr, sdkMsg) + if err != nil { + return nil, nil, err + } + // append data + data = append(data, res.Data) + // append events + sdkEvents := make([]sdk.Event, len(res.Events)) + for i := range res.Events { + sdkEvents[i] = sdk.Event(res.Events[i]) + } + events = append(events, sdkEvents...) + } + return +} + +func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) (*sdk.Result, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + // make sure this account can send it + for _, acct := range msg.GetSigners() { + if !acct.Equals(contractAddr) { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") + } + } + + // find the handler and execute it + if handler := h.router.Handler(msg); handler != nil { + // ADR 031 request type routing + msgResult, err := handler(ctx, msg) + return msgResult, err + } + // legacy sdk.Msg routing + // Assuming that the app developer has migrated all their Msgs to + // proto messages and has registered all `Msg services`, then this + // path should never be called, because all those Msgs should be + // registered within the `msgServiceRouter` already. + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg) +} + +// MessageHandlerChain defines a chain of handlers that are called one by one until it can be handled. +type MessageHandlerChain struct { + handlers []Messenger +} + +func NewMessageHandlerChain(first Messenger, others ...Messenger) *MessageHandlerChain { + r := &MessageHandlerChain{handlers: append([]Messenger{first}, others...)} + for i := range r.handlers { + if r.handlers[i] == nil { + panic(fmt.Sprintf("handler must not be nil at position : %d", i)) + } + } + return r +} + +// DispatchMsg dispatch message and calls chained handlers one after another in +// order to find the right one to process given message. If a handler cannot +// process given message (returns ErrUnknownMsg), its result is ignored and the +// next handler is executed. +func (m MessageHandlerChain) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, error) { + for _, h := range m.handlers { + events, data, err := h.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) + switch { + case err == nil: + return events, data, nil + case errors.Is(err, types.ErrUnknownMsg): + continue + default: + return events, data, err + } + } + return nil, nil, sdkerrors.Wrap(types.ErrUnknownMsg, "no handler found") +} + +// IBCRawPacketHandler handels IBC.SendPacket messages which are published to an IBC channel. +type IBCRawPacketHandler struct { + channelKeeper types.ChannelKeeper + capabilityKeeper types.CapabilityKeeper +} + +func NewIBCRawPacketHandler(chk types.ChannelKeeper, cak types.CapabilityKeeper) IBCRawPacketHandler { + return IBCRawPacketHandler{channelKeeper: chk, capabilityKeeper: cak} +} + +// DispatchMsg publishes a raw IBC packet onto the channel. +func (h IBCRawPacketHandler) DispatchMsg(ctx sdk.Context, _ sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + if msg.IBC == nil || msg.IBC.SendPacket == nil { + return nil, nil, types.ErrUnknownMsg + } + if contractIBCPortID == "" { + return nil, nil, sdkerrors.Wrapf(types.ErrUnsupportedForContract, "ibc not supported") + } + contractIBCChannelID := msg.IBC.SendPacket.ChannelID + if contractIBCChannelID == "" { + return nil, nil, sdkerrors.Wrapf(types.ErrEmpty, "ibc channel") + } + + sequence, found := h.channelKeeper.GetNextSequenceSend(ctx, contractIBCPortID, contractIBCChannelID) + if !found { + return nil, nil, sdkerrors.Wrapf(channeltypes.ErrSequenceSendNotFound, + "source port: %s, source channel: %s", contractIBCPortID, contractIBCChannelID, + ) + } + + channelInfo, ok := h.channelKeeper.GetChannel(ctx, contractIBCPortID, contractIBCChannelID) + if !ok { + return nil, nil, sdkerrors.Wrap(channeltypes.ErrInvalidChannel, "not found") + } + channelCap, ok := h.capabilityKeeper.GetCapability(ctx, host.ChannelCapabilityPath(contractIBCPortID, contractIBCChannelID)) + if !ok { + return nil, nil, sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") + } + packet := channeltypes.NewPacket( + msg.IBC.SendPacket.Data, + sequence, + contractIBCPortID, + contractIBCChannelID, + channelInfo.Counterparty.PortId, + channelInfo.Counterparty.ChannelId, + ConvertWasmIBCTimeoutHeightToCosmosHeight(msg.IBC.SendPacket.Timeout.Block), + msg.IBC.SendPacket.Timeout.Timestamp, + ) + return nil, nil, h.channelKeeper.SendPacket(ctx, channelCap, packet) +} + +var _ Messenger = MessageHandlerFunc(nil) + +// MessageHandlerFunc is a helper to construct a function based message handler. +type MessageHandlerFunc func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) + +// DispatchMsg delegates dispatching of provided message into the MessageHandlerFunc. +func (m MessageHandlerFunc) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return m(ctx, contractAddr, contractIBCPortID, msg) +} + +// NewBurnCoinMessageHandler handles wasmvm.BurnMsg messages +func NewBurnCoinMessageHandler(burner types.Burner) MessageHandlerFunc { + return func(ctx sdk.Context, contractAddr sdk.AccAddress, _ string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + if msg.Bank != nil && msg.Bank.Burn != nil { + coins, err := ConvertWasmCoinsToSdkCoins(msg.Bank.Burn.Amount) + if err != nil { + return nil, nil, err + } + if coins.IsZero() { + return nil, nil, types.ErrEmpty.Wrap("amount") + } + if err := burner.SendCoinsFromAccountToModule(ctx, contractAddr, types.ModuleName, coins); err != nil { + return nil, nil, sdkerrors.Wrap(err, "transfer to module") + } + if err := burner.BurnCoins(ctx, types.ModuleName, coins); err != nil { + return nil, nil, sdkerrors.Wrap(err, "burn coins") + } + moduleLogger(ctx).Info("Burned", "amount", coins) + return nil, nil, nil + } + return nil, nil, types.ErrUnknownMsg + } +} diff --git a/x/wasm/keeper/handler_plugin_encoders.go b/x/wasm/keeper/handler_plugin_encoders.go new file mode 100644 index 00000000..f23b24bb --- /dev/null +++ b/x/wasm/keeper/handler_plugin_encoders.go @@ -0,0 +1,393 @@ +package keeper + +import ( + "encoding/json" + "fmt" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + ibcclienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +type ( + BankEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error) + CustomEncoder func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) + DistributionEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.DistributionMsg) ([]sdk.Msg, error) + StakingEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.StakingMsg) ([]sdk.Msg, error) + StargateEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error) + WasmEncoder func(sender sdk.AccAddress, msg *wasmvmtypes.WasmMsg) ([]sdk.Msg, error) + IBCEncoder func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error) +) + +type MessageEncoders struct { + Bank func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error) + Custom func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) + Distribution func(sender sdk.AccAddress, msg *wasmvmtypes.DistributionMsg) ([]sdk.Msg, error) + IBC func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error) + Staking func(sender sdk.AccAddress, msg *wasmvmtypes.StakingMsg) ([]sdk.Msg, error) + Stargate func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error) + Wasm func(sender sdk.AccAddress, msg *wasmvmtypes.WasmMsg) ([]sdk.Msg, error) + Gov func(sender sdk.AccAddress, msg *wasmvmtypes.GovMsg) ([]sdk.Msg, error) +} + +func DefaultEncoders(unpacker codectypes.AnyUnpacker, portSource types.ICS20TransferPortSource) MessageEncoders { + return MessageEncoders{ + Bank: EncodeBankMsg, + Custom: NoCustomMsg, + Distribution: EncodeDistributionMsg, + IBC: EncodeIBCMsg(portSource), + Staking: EncodeStakingMsg, + Stargate: EncodeStargateMsg(unpacker), + Wasm: EncodeWasmMsg, + Gov: EncodeGovMsg, + } +} + +func (e MessageEncoders) Merge(o *MessageEncoders) MessageEncoders { + if o == nil { + return e + } + if o.Bank != nil { + e.Bank = o.Bank + } + if o.Custom != nil { + e.Custom = o.Custom + } + if o.Distribution != nil { + e.Distribution = o.Distribution + } + if o.IBC != nil { + e.IBC = o.IBC + } + if o.Staking != nil { + e.Staking = o.Staking + } + if o.Stargate != nil { + e.Stargate = o.Stargate + } + if o.Wasm != nil { + e.Wasm = o.Wasm + } + if o.Gov != nil { + e.Gov = o.Gov + } + return e +} + +func (e MessageEncoders) Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error) { + switch { + case msg.Bank != nil: + return e.Bank(contractAddr, msg.Bank) + case msg.Custom != nil: + return e.Custom(contractAddr, msg.Custom) + case msg.Distribution != nil: + return e.Distribution(contractAddr, msg.Distribution) + case msg.IBC != nil: + return e.IBC(ctx, contractAddr, contractIBCPortID, msg.IBC) + case msg.Staking != nil: + return e.Staking(contractAddr, msg.Staking) + case msg.Stargate != nil: + return e.Stargate(contractAddr, msg.Stargate) + case msg.Wasm != nil: + return e.Wasm(contractAddr, msg.Wasm) + case msg.Gov != nil: + return EncodeGovMsg(contractAddr, msg.Gov) + } + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Wasm") +} + +func EncodeBankMsg(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error) { + if msg.Send == nil { + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Bank") + } + if len(msg.Send.Amount) == 0 { + return nil, nil + } + toSend, err := ConvertWasmCoinsToSdkCoins(msg.Send.Amount) + if err != nil { + return nil, err + } + sdkMsg := banktypes.MsgSend{ + FromAddress: sender.String(), + ToAddress: msg.Send.ToAddress, + Amount: toSend, + } + return []sdk.Msg{&sdkMsg}, nil +} + +func NoCustomMsg(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "custom variant not supported") +} + +func EncodeDistributionMsg(sender sdk.AccAddress, msg *wasmvmtypes.DistributionMsg) ([]sdk.Msg, error) { + switch { + case msg.SetWithdrawAddress != nil: + setMsg := distributiontypes.MsgSetWithdrawAddress{ + DelegatorAddress: sender.String(), + WithdrawAddress: msg.SetWithdrawAddress.Address, + } + return []sdk.Msg{&setMsg}, nil + case msg.WithdrawDelegatorReward != nil: + withdrawMsg := distributiontypes.MsgWithdrawDelegatorReward{ + DelegatorAddress: sender.String(), + ValidatorAddress: msg.WithdrawDelegatorReward.Validator, + } + return []sdk.Msg{&withdrawMsg}, nil + default: + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Distribution") + } +} + +func EncodeStakingMsg(sender sdk.AccAddress, msg *wasmvmtypes.StakingMsg) ([]sdk.Msg, error) { + switch { + case msg.Delegate != nil: + coin, err := ConvertWasmCoinToSdkCoin(msg.Delegate.Amount) + if err != nil { + return nil, err + } + sdkMsg := stakingtypes.MsgDelegate{ + DelegatorAddress: sender.String(), + ValidatorAddress: msg.Delegate.Validator, + Amount: coin, + } + return []sdk.Msg{&sdkMsg}, nil + + case msg.Redelegate != nil: + coin, err := ConvertWasmCoinToSdkCoin(msg.Redelegate.Amount) + if err != nil { + return nil, err + } + sdkMsg := stakingtypes.MsgBeginRedelegate{ + DelegatorAddress: sender.String(), + ValidatorSrcAddress: msg.Redelegate.SrcValidator, + ValidatorDstAddress: msg.Redelegate.DstValidator, + Amount: coin, + } + return []sdk.Msg{&sdkMsg}, nil + case msg.Undelegate != nil: + coin, err := ConvertWasmCoinToSdkCoin(msg.Undelegate.Amount) + if err != nil { + return nil, err + } + sdkMsg := stakingtypes.MsgUndelegate{ + DelegatorAddress: sender.String(), + ValidatorAddress: msg.Undelegate.Validator, + Amount: coin, + } + return []sdk.Msg{&sdkMsg}, nil + default: + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Staking") + } +} + +func EncodeStargateMsg(unpacker codectypes.AnyUnpacker) StargateEncoder { + return func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error) { + any := codectypes.Any{ + TypeUrl: msg.TypeURL, + Value: msg.Value, + } + var sdkMsg sdk.Msg + if err := unpacker.UnpackAny(&any, &sdkMsg); err != nil { + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, fmt.Sprintf("Cannot unpack proto message with type URL: %s", msg.TypeURL)) + } + if err := codectypes.UnpackInterfaces(sdkMsg, unpacker); err != nil { + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, fmt.Sprintf("UnpackInterfaces inside msg: %s", err)) + } + return []sdk.Msg{sdkMsg}, nil + } +} + +func EncodeWasmMsg(sender sdk.AccAddress, msg *wasmvmtypes.WasmMsg) ([]sdk.Msg, error) { + switch { + case msg.Execute != nil: + coins, err := ConvertWasmCoinsToSdkCoins(msg.Execute.Funds) + if err != nil { + return nil, err + } + + sdkMsg := types.MsgExecuteContract{ + Sender: sender.String(), + Contract: msg.Execute.ContractAddr, + Msg: msg.Execute.Msg, + Funds: coins, + } + return []sdk.Msg{&sdkMsg}, nil + case msg.Instantiate != nil: + coins, err := ConvertWasmCoinsToSdkCoins(msg.Instantiate.Funds) + if err != nil { + return nil, err + } + + sdkMsg := types.MsgInstantiateContract{ + Sender: sender.String(), + CodeID: msg.Instantiate.CodeID, + Label: msg.Instantiate.Label, + Msg: msg.Instantiate.Msg, + Admin: msg.Instantiate.Admin, + Funds: coins, + } + return []sdk.Msg{&sdkMsg}, nil + case msg.Instantiate2 != nil: + coins, err := ConvertWasmCoinsToSdkCoins(msg.Instantiate2.Funds) + if err != nil { + return nil, err + } + + sdkMsg := types.MsgInstantiateContract2{ + Sender: sender.String(), + Admin: msg.Instantiate2.Admin, + CodeID: msg.Instantiate2.CodeID, + Label: msg.Instantiate2.Label, + Msg: msg.Instantiate2.Msg, + Funds: coins, + Salt: msg.Instantiate2.Salt, + // FixMsg is discouraged, see: https://medium.com/cosmwasm/dev-note-3-limitations-of-instantiate2-and-how-to-deal-with-them-a3f946874230 + FixMsg: false, + } + return []sdk.Msg{&sdkMsg}, nil + case msg.Migrate != nil: + sdkMsg := types.MsgMigrateContract{ + Sender: sender.String(), + Contract: msg.Migrate.ContractAddr, + CodeID: msg.Migrate.NewCodeID, + Msg: msg.Migrate.Msg, + } + return []sdk.Msg{&sdkMsg}, nil + case msg.ClearAdmin != nil: + sdkMsg := types.MsgClearAdmin{ + Sender: sender.String(), + Contract: msg.ClearAdmin.ContractAddr, + } + return []sdk.Msg{&sdkMsg}, nil + case msg.UpdateAdmin != nil: + sdkMsg := types.MsgUpdateAdmin{ + Sender: sender.String(), + Contract: msg.UpdateAdmin.ContractAddr, + NewAdmin: msg.UpdateAdmin.Admin, + } + return []sdk.Msg{&sdkMsg}, nil + default: + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of Wasm") + } +} + +func EncodeIBCMsg(portSource types.ICS20TransferPortSource) func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error) { + return func(ctx sdk.Context, sender sdk.AccAddress, contractIBCPortID string, msg *wasmvmtypes.IBCMsg) ([]sdk.Msg, error) { + switch { + case msg.CloseChannel != nil: + return []sdk.Msg{&channeltypes.MsgChannelCloseInit{ + PortId: PortIDForContract(sender), + ChannelId: msg.CloseChannel.ChannelID, + Signer: sender.String(), + }}, nil + case msg.Transfer != nil: + amount, err := ConvertWasmCoinToSdkCoin(msg.Transfer.Amount) + if err != nil { + return nil, sdkerrors.Wrap(err, "amount") + } + msg := &ibctransfertypes.MsgTransfer{ + SourcePort: portSource.GetPort(ctx), + SourceChannel: msg.Transfer.ChannelID, + Token: amount, + Sender: sender.String(), + Receiver: msg.Transfer.ToAddress, + TimeoutHeight: ConvertWasmIBCTimeoutHeightToCosmosHeight(msg.Transfer.Timeout.Block), + TimeoutTimestamp: msg.Transfer.Timeout.Timestamp, + } + return []sdk.Msg{msg}, nil + default: + return nil, sdkerrors.Wrap(types.ErrUnknownMsg, "unknown variant of IBC") + } + } +} + +func EncodeGovMsg(sender sdk.AccAddress, msg *wasmvmtypes.GovMsg) ([]sdk.Msg, error) { + switch { + case msg.Vote != nil: + voteOption, err := convertVoteOption(msg.Vote.Vote) + if err != nil { + return nil, sdkerrors.Wrap(err, "vote option") + } + m := govtypes.NewMsgVote(sender, msg.Vote.ProposalId, voteOption) + return []sdk.Msg{m}, nil + case msg.VoteWeighted != nil: + opts := make([]govtypes.WeightedVoteOption, len(msg.VoteWeighted.Options)) + for i, v := range msg.VoteWeighted.Options { + weight, err := sdk.NewDecFromStr(v.Weight) + if err != nil { + return nil, sdkerrors.Wrapf(err, "weight for vote %d", i+1) + } + voteOption, err := convertVoteOption(v.Option) + if err != nil { + return nil, sdkerrors.Wrap(err, "vote option") + } + opts[i] = govtypes.WeightedVoteOption{Option: voteOption, Weight: weight} + } + m := govtypes.NewMsgVoteWeighted(sender, msg.VoteWeighted.ProposalId, opts) + return []sdk.Msg{m}, nil + + default: + return nil, types.ErrUnknownMsg.Wrap("unknown variant of gov") + } +} + +func convertVoteOption(s interface{}) (govtypes.VoteOption, error) { + var option govtypes.VoteOption + switch s { + case wasmvmtypes.Yes: + option = govtypes.OptionYes + case wasmvmtypes.No: + option = govtypes.OptionNo + case wasmvmtypes.NoWithVeto: + option = govtypes.OptionNoWithVeto + case wasmvmtypes.Abstain: + option = govtypes.OptionAbstain + default: + return govtypes.OptionEmpty, types.ErrInvalid + } + return option, nil +} + +// ConvertWasmIBCTimeoutHeightToCosmosHeight converts a wasmvm type ibc timeout height to ibc module type height +func ConvertWasmIBCTimeoutHeightToCosmosHeight(ibcTimeoutBlock *wasmvmtypes.IBCTimeoutBlock) ibcclienttypes.Height { + if ibcTimeoutBlock == nil { + return ibcclienttypes.NewHeight(0, 0) + } + return ibcclienttypes.NewHeight(ibcTimeoutBlock.Revision, ibcTimeoutBlock.Height) +} + +// ConvertWasmCoinsToSdkCoins converts the wasm vm type coins to sdk type coins +func ConvertWasmCoinsToSdkCoins(coins []wasmvmtypes.Coin) (sdk.Coins, error) { + var toSend sdk.Coins + for _, coin := range coins { + c, err := ConvertWasmCoinToSdkCoin(coin) + if err != nil { + return nil, err + } + toSend = toSend.Add(c) + } + return toSend.Sort(), nil +} + +// ConvertWasmCoinToSdkCoin converts a wasm vm type coin to sdk type coin +func ConvertWasmCoinToSdkCoin(coin wasmvmtypes.Coin) (sdk.Coin, error) { + amount, ok := sdk.NewIntFromString(coin.Amount) + if !ok { + return sdk.Coin{}, sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, coin.Amount+coin.Denom) + } + r := sdk.Coin{ + Denom: coin.Denom, + Amount: amount, + } + return r, r.Validate() +} diff --git a/x/wasm/keeper/handler_plugin_encoders_test.go b/x/wasm/keeper/handler_plugin_encoders_test.go new file mode 100644 index 00000000..eb33630c --- /dev/null +++ b/x/wasm/keeper/handler_plugin_encoders_test.go @@ -0,0 +1,932 @@ +package keeper + +import ( + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestEncoding(t *testing.T) { + var ( + addr1 = RandomAccountAddress(t) + addr2 = RandomAccountAddress(t) + addr3 = RandomAccountAddress(t) + invalidAddr = "xrnd1d02kd90n38qvr3qb9qof83fn2d2" + ) + valAddr := make(sdk.ValAddress, types.SDKAddrLen) + valAddr[0] = 12 + valAddr2 := make(sdk.ValAddress, types.SDKAddrLen) + valAddr2[1] = 123 + + jsonMsg := types.RawContractMessage(`{"foo": 123}`) + + bankMsg := &banktypes.MsgSend{ + FromAddress: addr2.String(), + ToAddress: addr1.String(), + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 12345), + sdk.NewInt64Coin("utgd", 54321), + }, + } + bankMsgBin, err := proto.Marshal(bankMsg) + require.NoError(t, err) + + content, err := codectypes.NewAnyWithValue(types.StoreCodeProposalFixture()) + require.NoError(t, err) + + proposalMsg := &govtypes.MsgSubmitProposal{ + Proposer: addr1.String(), + InitialDeposit: sdk.NewCoins(sdk.NewInt64Coin("uatom", 12345)), + Content: content, + } + proposalMsgBin, err := proto.Marshal(proposalMsg) + require.NoError(t, err) + + cases := map[string]struct { + sender sdk.AccAddress + srcMsg wasmvmtypes.CosmosMsg + srcContractIBCPort string + transferPortSource types.ICS20TransferPortSource + // set if valid + output []sdk.Msg + // set if expect mapping fails + expError bool + // set if sdk validate basic should fail + expInvalid bool + }{ + "simple send": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: addr2.String(), + Amount: []wasmvmtypes.Coin{ + { + Denom: "uatom", + Amount: "12345", + }, + { + Denom: "usdt", + Amount: "54321", + }, + }, + }, + }, + }, + output: []sdk.Msg{ + &banktypes.MsgSend{ + FromAddress: addr1.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 12345), + sdk.NewInt64Coin("usdt", 54321), + }, + }, + }, + }, + "invalid send amount": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: addr2.String(), + Amount: []wasmvmtypes.Coin{ + { + Denom: "uatom", + Amount: "123.456", + }, + }, + }, + }, + }, + expError: true, + }, + "invalid address": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: invalidAddr, + Amount: []wasmvmtypes.Coin{ + { + Denom: "uatom", + Amount: "7890", + }, + }, + }, + }, + }, + expError: false, // addresses are checked in the handler + expInvalid: true, + output: []sdk.Msg{ + &banktypes.MsgSend{ + FromAddress: addr1.String(), + ToAddress: invalidAddr, + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 7890), + }, + }, + }, + }, + "wasm execute": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Execute: &wasmvmtypes.ExecuteMsg{ + ContractAddr: addr2.String(), + Msg: jsonMsg, + Funds: []wasmvmtypes.Coin{ + wasmvmtypes.NewCoin(12, "eth"), + }, + }, + }, + }, + output: []sdk.Msg{ + &types.MsgExecuteContract{ + Sender: addr1.String(), + Contract: addr2.String(), + Msg: jsonMsg, + Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 12)), + }, + }, + }, + "wasm instantiate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Instantiate: &wasmvmtypes.InstantiateMsg{ + CodeID: 7, + Msg: jsonMsg, + Funds: []wasmvmtypes.Coin{ + wasmvmtypes.NewCoin(123, "eth"), + }, + Label: "myLabel", + Admin: addr2.String(), + }, + }, + }, + output: []sdk.Msg{ + &types.MsgInstantiateContract{ + Sender: addr1.String(), + CodeID: 7, + Label: "myLabel", + Msg: jsonMsg, + Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 123)), + Admin: addr2.String(), + }, + }, + }, + "wasm instantiate2": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Instantiate2: &wasmvmtypes.Instantiate2Msg{ + CodeID: 7, + Msg: jsonMsg, + Funds: []wasmvmtypes.Coin{ + wasmvmtypes.NewCoin(123, "eth"), + }, + Label: "myLabel", + Admin: addr2.String(), + Salt: []byte("mySalt"), + }, + }, + }, + output: []sdk.Msg{ + &types.MsgInstantiateContract2{ + Sender: addr1.String(), + Admin: addr2.String(), + CodeID: 7, + Label: "myLabel", + Msg: jsonMsg, + Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 123)), + Salt: []byte("mySalt"), + FixMsg: false, + }, + }, + }, + "wasm migrate": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Migrate: &wasmvmtypes.MigrateMsg{ + ContractAddr: addr1.String(), + NewCodeID: 12, + Msg: jsonMsg, + }, + }, + }, + output: []sdk.Msg{ + &types.MsgMigrateContract{ + Sender: addr2.String(), + Contract: addr1.String(), + CodeID: 12, + Msg: jsonMsg, + }, + }, + }, + "wasm update admin": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + UpdateAdmin: &wasmvmtypes.UpdateAdminMsg{ + ContractAddr: addr1.String(), + Admin: addr3.String(), + }, + }, + }, + output: []sdk.Msg{ + &types.MsgUpdateAdmin{ + Sender: addr2.String(), + Contract: addr1.String(), + NewAdmin: addr3.String(), + }, + }, + }, + "wasm clear admin": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + ClearAdmin: &wasmvmtypes.ClearAdminMsg{ + ContractAddr: addr1.String(), + }, + }, + }, + output: []sdk.Msg{ + &types.MsgClearAdmin{ + Sender: addr2.String(), + Contract: addr1.String(), + }, + }, + }, + "staking delegate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Delegate: &wasmvmtypes.DelegateMsg{ + Validator: valAddr.String(), + Amount: wasmvmtypes.NewCoin(777, "stake"), + }, + }, + }, + output: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: addr1.String(), + ValidatorAddress: valAddr.String(), + Amount: sdk.NewInt64Coin("stake", 777), + }, + }, + }, + "staking delegate to non-validator": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Delegate: &wasmvmtypes.DelegateMsg{ + Validator: addr2.String(), + Amount: wasmvmtypes.NewCoin(777, "stake"), + }, + }, + }, + expError: false, // fails in the handler + output: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: addr1.String(), + ValidatorAddress: addr2.String(), + Amount: sdk.NewInt64Coin("stake", 777), + }, + }, + }, + "staking undelegate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Undelegate: &wasmvmtypes.UndelegateMsg{ + Validator: valAddr.String(), + Amount: wasmvmtypes.NewCoin(555, "stake"), + }, + }, + }, + output: []sdk.Msg{ + &stakingtypes.MsgUndelegate{ + DelegatorAddress: addr1.String(), + ValidatorAddress: valAddr.String(), + Amount: sdk.NewInt64Coin("stake", 555), + }, + }, + }, + "staking redelegate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Redelegate: &wasmvmtypes.RedelegateMsg{ + SrcValidator: valAddr.String(), + DstValidator: valAddr2.String(), + Amount: wasmvmtypes.NewCoin(222, "stake"), + }, + }, + }, + output: []sdk.Msg{ + &stakingtypes.MsgBeginRedelegate{ + DelegatorAddress: addr1.String(), + ValidatorSrcAddress: valAddr.String(), + ValidatorDstAddress: valAddr2.String(), + Amount: sdk.NewInt64Coin("stake", 222), + }, + }, + }, + "staking withdraw (explicit recipient)": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Distribution: &wasmvmtypes.DistributionMsg{ + WithdrawDelegatorReward: &wasmvmtypes.WithdrawDelegatorRewardMsg{ + Validator: valAddr2.String(), + }, + }, + }, + output: []sdk.Msg{ + &distributiontypes.MsgWithdrawDelegatorReward{ + DelegatorAddress: addr1.String(), + ValidatorAddress: valAddr2.String(), + }, + }, + }, + "staking set withdraw address": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Distribution: &wasmvmtypes.DistributionMsg{ + SetWithdrawAddress: &wasmvmtypes.SetWithdrawAddressMsg{ + Address: addr2.String(), + }, + }, + }, + output: []sdk.Msg{ + &distributiontypes.MsgSetWithdrawAddress{ + DelegatorAddress: addr1.String(), + WithdrawAddress: addr2.String(), + }, + }, + }, + "stargate encoded bank msg": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/cosmos.bank.v1beta1.MsgSend", + Value: bankMsgBin, + }, + }, + output: []sdk.Msg{bankMsg}, + }, + "stargate encoded msg with any type": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/cosmos.gov.v1beta1.MsgSubmitProposal", + Value: proposalMsgBin, + }, + }, + output: []sdk.Msg{proposalMsg}, + }, + "stargate encoded invalid typeUrl": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/cosmos.bank.v2.MsgSend", + Value: bankMsgBin, + }, + }, + expError: true, + }, + "IBC transfer with block timeout": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ChannelID: "myChanID", + ToAddress: addr2.String(), + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + Timeout: wasmvmtypes.IBCTimeout{ + Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}, + }, + }, + }, + }, + transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { + return "myTransferPort" + }}, + output: []sdk.Msg{ + &ibctransfertypes.MsgTransfer{ + SourcePort: "myTransferPort", + SourceChannel: "myChanID", + Token: sdk.Coin{ + Denom: "ALX", + Amount: sdk.NewInt(1), + }, + Sender: addr1.String(), + Receiver: addr2.String(), + TimeoutHeight: clienttypes.Height{RevisionNumber: 1, RevisionHeight: 2}, + }, + }, + }, + "IBC transfer with time timeout": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ChannelID: "myChanID", + ToAddress: addr2.String(), + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + Timeout: wasmvmtypes.IBCTimeout{Timestamp: 100}, + }, + }, + }, + transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { + return "transfer" + }}, + output: []sdk.Msg{ + &ibctransfertypes.MsgTransfer{ + SourcePort: "transfer", + SourceChannel: "myChanID", + Token: sdk.Coin{ + Denom: "ALX", + Amount: sdk.NewInt(1), + }, + Sender: addr1.String(), + Receiver: addr2.String(), + TimeoutTimestamp: 100, + }, + }, + }, + "IBC transfer with time and height timeout": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ChannelID: "myChanID", + ToAddress: addr2.String(), + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + Timeout: wasmvmtypes.IBCTimeout{Timestamp: 100, Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2}}, + }, + }, + }, + transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { + return "transfer" + }}, + output: []sdk.Msg{ + &ibctransfertypes.MsgTransfer{ + SourcePort: "transfer", + SourceChannel: "myChanID", + Token: sdk.Coin{ + Denom: "ALX", + Amount: sdk.NewInt(1), + }, + Sender: addr1.String(), + Receiver: addr2.String(), + TimeoutTimestamp: 100, + TimeoutHeight: clienttypes.NewHeight(2, 1), + }, + }, + }, + "IBC close channel": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + CloseChannel: &wasmvmtypes.CloseChannelMsg{ + ChannelID: "channel-1", + }, + }, + }, + output: []sdk.Msg{ + &channeltypes.MsgChannelCloseInit{ + PortId: "wasm." + addr1.String(), + ChannelId: "channel-1", + Signer: addr1.String(), + }, + }, + }, + } + encodingConfig := MakeEncodingConfig(t) + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var ctx sdk.Context + encoder := DefaultEncoders(encodingConfig.Marshaler, tc.transferPortSource) + res, err := encoder.Encode(ctx, tc.sender, tc.srcContractIBCPort, tc.srcMsg) + if tc.expError { + assert.Error(t, err) + return + } else { + require.NoError(t, err) + assert.Equal(t, tc.output, res) + } + // and valid sdk message + for _, v := range res { + gotErr := v.ValidateBasic() + if tc.expInvalid { + assert.Error(t, gotErr) + } else { + assert.NoError(t, gotErr) + } + } + }) + } +} + +func TestEncodeGovMsg(t *testing.T) { + myAddr := RandomAccountAddress(t) + + cases := map[string]struct { + sender sdk.AccAddress + srcMsg wasmvmtypes.CosmosMsg + transferPortSource types.ICS20TransferPortSource + // set if valid + output []sdk.Msg + // set if expect mapping fails + expError bool + // set if sdk validate basic should fail + expInvalid bool + }{ + "Gov vote: yes": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.Yes}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 1, + Voter: myAddr.String(), + Option: govtypes.OptionYes, + }, + }, + }, + "Gov vote: No": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.No}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 1, + Voter: myAddr.String(), + Option: govtypes.OptionNo, + }, + }, + }, + "Gov vote: Abstain": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 10, Vote: wasmvmtypes.Abstain}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 10, + Voter: myAddr.String(), + Option: govtypes.OptionAbstain, + }, + }, + }, + "Gov vote: No with veto": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.NoWithVeto}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 1, + Voter: myAddr.String(), + Option: govtypes.OptionNoWithVeto, + }, + }, + }, + "Gov vote: unset option": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1}, + }, + }, + expError: true, + }, + "Gov weighted vote: single vote": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + VoteWeighted: &wasmvmtypes.VoteWeightedMsg{ + ProposalId: 1, + Options: []wasmvmtypes.WeightedVoteOption{ + {Option: wasmvmtypes.Yes, Weight: "1"}, + }, + }, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVoteWeighted{ + ProposalId: 1, + Voter: myAddr.String(), + Options: []govtypes.WeightedVoteOption{ + {Option: govtypes.OptionYes, Weight: sdk.NewDec(1)}, + }, + }, + }, + }, + "Gov weighted vote: splitted": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + VoteWeighted: &wasmvmtypes.VoteWeightedMsg{ + ProposalId: 1, + Options: []wasmvmtypes.WeightedVoteOption{ + {Option: wasmvmtypes.Yes, Weight: "0.23"}, + {Option: wasmvmtypes.No, Weight: "0.24"}, + {Option: wasmvmtypes.Abstain, Weight: "0.26"}, + {Option: wasmvmtypes.NoWithVeto, Weight: "0.27"}, + }, + }, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVoteWeighted{ + ProposalId: 1, + Voter: myAddr.String(), + Options: []govtypes.WeightedVoteOption{ + {Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(23, 2)}, + {Option: govtypes.OptionNo, Weight: sdk.NewDecWithPrec(24, 2)}, + {Option: govtypes.OptionAbstain, Weight: sdk.NewDecWithPrec(26, 2)}, + {Option: govtypes.OptionNoWithVeto, Weight: sdk.NewDecWithPrec(27, 2)}, + }, + }, + }, + }, + "Gov weighted vote: duplicate option": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + VoteWeighted: &wasmvmtypes.VoteWeightedMsg{ + ProposalId: 1, + Options: []wasmvmtypes.WeightedVoteOption{ + {Option: wasmvmtypes.Yes, Weight: "0.5"}, + {Option: wasmvmtypes.Yes, Weight: "0.5"}, + }, + }, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVoteWeighted{ + ProposalId: 1, + Voter: myAddr.String(), + Options: []govtypes.WeightedVoteOption{ + {Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(5, 1)}, + {Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(5, 1)}, + }, + }, + }, + expInvalid: true, + }, + "Gov weighted vote: weight sum exceeds 1": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + VoteWeighted: &wasmvmtypes.VoteWeightedMsg{ + ProposalId: 1, + Options: []wasmvmtypes.WeightedVoteOption{ + {Option: wasmvmtypes.Yes, Weight: "0.51"}, + {Option: wasmvmtypes.No, Weight: "0.5"}, + }, + }, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVoteWeighted{ + ProposalId: 1, + Voter: myAddr.String(), + Options: []govtypes.WeightedVoteOption{ + {Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(51, 2)}, + {Option: govtypes.OptionNo, Weight: sdk.NewDecWithPrec(5, 1)}, + }, + }, + }, + expInvalid: true, + }, + "Gov weighted vote: weight sum less than 1": { + sender: myAddr, + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + VoteWeighted: &wasmvmtypes.VoteWeightedMsg{ + ProposalId: 1, + Options: []wasmvmtypes.WeightedVoteOption{ + {Option: wasmvmtypes.Yes, Weight: "0.49"}, + {Option: wasmvmtypes.No, Weight: "0.5"}, + }, + }, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVoteWeighted{ + ProposalId: 1, + Voter: myAddr.String(), + Options: []govtypes.WeightedVoteOption{ + {Option: govtypes.OptionYes, Weight: sdk.NewDecWithPrec(49, 2)}, + {Option: govtypes.OptionNo, Weight: sdk.NewDecWithPrec(5, 1)}, + }, + }, + }, + expInvalid: true, + }, + } + encodingConfig := MakeEncodingConfig(t) + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var ctx sdk.Context + encoder := DefaultEncoders(encodingConfig.Marshaler, tc.transferPortSource) + res, gotEncErr := encoder.Encode(ctx, tc.sender, "myIBCPort", tc.srcMsg) + if tc.expError { + assert.Error(t, gotEncErr) + return + } else { + require.NoError(t, gotEncErr) + assert.Equal(t, tc.output, res) + } + // and valid sdk message + for _, v := range res { + gotErr := v.ValidateBasic() + if tc.expInvalid { + assert.Error(t, gotErr) + } else { + assert.NoError(t, gotErr) + } + } + }) + } +} + +func TestConvertWasmCoinToSdkCoin(t *testing.T) { + specs := map[string]struct { + src wasmvmtypes.Coin + expErr bool + expVal sdk.Coin + }{ + "all good": { + src: wasmvmtypes.Coin{ + Denom: "foo", + Amount: "1", + }, + expVal: sdk.NewCoin("foo", sdk.NewIntFromUint64(1)), + }, + "negative amount": { + src: wasmvmtypes.Coin{ + Denom: "foo", + Amount: "-1", + }, + expErr: true, + }, + "denom to short": { + src: wasmvmtypes.Coin{ + Denom: "f", + Amount: "1", + }, + expErr: true, + }, + "invalid demum char": { + src: wasmvmtypes.Coin{ + Denom: "&fff", + Amount: "1", + }, + expErr: true, + }, + "not a number amount": { + src: wasmvmtypes.Coin{ + Denom: "foo", + Amount: "bar", + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotVal, gotErr := ConvertWasmCoinToSdkCoin(spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expVal, gotVal) + }) + } +} + +func TestConvertWasmCoinsToSdkCoins(t *testing.T) { + specs := map[string]struct { + src []wasmvmtypes.Coin + exp sdk.Coins + expErr bool + }{ + "empty": { + src: []wasmvmtypes.Coin{}, + exp: nil, + }, + "single coin": { + src: []wasmvmtypes.Coin{{Denom: "foo", Amount: "1"}}, + exp: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1))), + }, + "multiple coins": { + src: []wasmvmtypes.Coin{ + {Denom: "foo", Amount: "1"}, + {Denom: "bar", Amount: "2"}, + }, + exp: sdk.NewCoins( + sdk.NewCoin("bar", sdk.NewInt(2)), + sdk.NewCoin("foo", sdk.NewInt(1)), + ), + }, + "sorted": { + src: []wasmvmtypes.Coin{ + {Denom: "foo", Amount: "1"}, + {Denom: "other", Amount: "1"}, + {Denom: "bar", Amount: "1"}, + }, + exp: []sdk.Coin{ + sdk.NewCoin("bar", sdk.NewInt(1)), + sdk.NewCoin("foo", sdk.NewInt(1)), + sdk.NewCoin("other", sdk.NewInt(1)), + }, + }, + "zero amounts dropped": { + src: []wasmvmtypes.Coin{ + {Denom: "foo", Amount: "1"}, + {Denom: "bar", Amount: "0"}, + }, + exp: sdk.NewCoins( + sdk.NewCoin("foo", sdk.NewInt(1)), + ), + }, + "duplicate denoms merged": { + src: []wasmvmtypes.Coin{ + {Denom: "foo", Amount: "1"}, + {Denom: "foo", Amount: "1"}, + }, + exp: []sdk.Coin{sdk.NewCoin("foo", sdk.NewInt(2))}, + }, + "duplicate denoms with one 0 amount does not fail": { + src: []wasmvmtypes.Coin{ + {Denom: "foo", Amount: "0"}, + {Denom: "foo", Amount: "1"}, + }, + exp: []sdk.Coin{sdk.NewCoin("foo", sdk.NewInt(1))}, + }, + "empty denom rejected": { + src: []wasmvmtypes.Coin{{Denom: "", Amount: "1"}}, + expErr: true, + }, + "invalid denom rejected": { + src: []wasmvmtypes.Coin{{Denom: "!%&", Amount: "1"}}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotCoins, gotErr := ConvertWasmCoinsToSdkCoins(spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, gotCoins) + assert.NoError(t, gotCoins.Validate()) + }) + } +} diff --git a/x/wasm/keeper/handler_plugin_test.go b/x/wasm/keeper/handler_plugin_test.go new file mode 100644 index 00000000..b1a76d69 --- /dev/null +++ b/x/wasm/keeper/handler_plugin_test.go @@ -0,0 +1,410 @@ +package keeper + +import ( + "encoding/json" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestMessageHandlerChainDispatch(t *testing.T) { + capturingHandler, gotMsgs := wasmtesting.NewCapturingMessageHandler() + + alwaysUnknownMsgHandler := &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, types.ErrUnknownMsg + }, + } + + assertNotCalledHandler := &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + t.Fatal("not expected to be called") + return + }, + } + + myMsg := wasmvmtypes.CosmosMsg{Custom: []byte(`{}`)} + specs := map[string]struct { + handlers []Messenger + expErr *sdkerrors.Error + expEvents []sdk.Event + }{ + "single handler": { + handlers: []Messenger{capturingHandler}, + }, + "passed to next handler": { + handlers: []Messenger{alwaysUnknownMsgHandler, capturingHandler}, + }, + "stops iteration when handled": { + handlers: []Messenger{capturingHandler, assertNotCalledHandler}, + }, + "stops iteration on handler error": { + handlers: []Messenger{&wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, types.ErrInvalidMsg + }, + }, assertNotCalledHandler}, + expErr: types.ErrInvalidMsg, + }, + "return events when handle": { + handlers: []Messenger{ + &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + _, data, _ = capturingHandler.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) + return []sdk.Event{sdk.NewEvent("myEvent", sdk.NewAttribute("foo", "bar"))}, data, nil + }, + }, + }, + expEvents: []sdk.Event{sdk.NewEvent("myEvent", sdk.NewAttribute("foo", "bar"))}, + }, + "return error when none can handle": { + handlers: []Messenger{alwaysUnknownMsgHandler}, + expErr: types.ErrUnknownMsg, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + *gotMsgs = make([]wasmvmtypes.CosmosMsg, 0) + + // when + h := MessageHandlerChain{spec.handlers} + gotEvents, gotData, gotErr := h.DispatchMsg(sdk.Context{}, RandomAccountAddress(t), "anyPort", myMsg) + + // then + require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr) + if spec.expErr != nil { + return + } + assert.Equal(t, []wasmvmtypes.CosmosMsg{myMsg}, *gotMsgs) + assert.Equal(t, [][]byte{{1}}, gotData) // {1} is default in capturing handler + assert.Equal(t, spec.expEvents, gotEvents) + }) + } +} + +func TestSDKMessageHandlerDispatch(t *testing.T) { + myEvent := sdk.NewEvent("myEvent", sdk.NewAttribute("foo", "bar")) + const myData = "myData" + myRouterResult := sdk.Result{ + Data: []byte(myData), + Events: sdk.Events{myEvent}.ToABCIEvents(), + } + + var gotMsg []sdk.Msg + capturingMessageRouter := wasmtesting.MessageRouterFunc(func(msg sdk.Msg) baseapp.MsgServiceHandler { + return func(ctx sdk.Context, req sdk.Msg) (*sdk.Result, error) { + gotMsg = append(gotMsg, msg) + return &myRouterResult, nil + } + }) + noRouteMessageRouter := wasmtesting.MessageRouterFunc(func(msg sdk.Msg) baseapp.MsgServiceHandler { + return nil + }) + myContractAddr := RandomAccountAddress(t) + myContractMessage := wasmvmtypes.CosmosMsg{Custom: []byte("{}")} + + specs := map[string]struct { + srcRoute MessageRouter + srcEncoder CustomEncoder + expErr *sdkerrors.Error + expMsgDispatched int + }{ + "all good": { + srcRoute: capturingMessageRouter, + srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + myMsg := types.MsgExecuteContract{ + Sender: myContractAddr.String(), + Contract: RandomBech32AccountAddress(t), + Msg: []byte("{}"), + } + return []sdk.Msg{&myMsg}, nil + }, + expMsgDispatched: 1, + }, + "multiple output msgs": { + srcRoute: capturingMessageRouter, + srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + first := &types.MsgExecuteContract{ + Sender: myContractAddr.String(), + Contract: RandomBech32AccountAddress(t), + Msg: []byte("{}"), + } + second := &types.MsgExecuteContract{ + Sender: myContractAddr.String(), + Contract: RandomBech32AccountAddress(t), + Msg: []byte("{}"), + } + return []sdk.Msg{first, second}, nil + }, + expMsgDispatched: 2, + }, + "invalid sdk message rejected": { + srcRoute: capturingMessageRouter, + srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + invalidMsg := types.MsgExecuteContract{ + Sender: myContractAddr.String(), + Contract: RandomBech32AccountAddress(t), + Msg: []byte("INVALID_JSON"), + } + return []sdk.Msg{&invalidMsg}, nil + }, + expErr: types.ErrInvalid, + }, + "invalid sender rejected": { + srcRoute: capturingMessageRouter, + srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + invalidMsg := types.MsgExecuteContract{ + Sender: RandomBech32AccountAddress(t), + Contract: RandomBech32AccountAddress(t), + Msg: []byte("{}"), + } + return []sdk.Msg{&invalidMsg}, nil + }, + expErr: sdkerrors.ErrUnauthorized, + }, + "unroutable message rejected": { + srcRoute: noRouteMessageRouter, + srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + myMsg := types.MsgExecuteContract{ + Sender: myContractAddr.String(), + Contract: RandomBech32AccountAddress(t), + Msg: []byte("{}"), + } + return []sdk.Msg{&myMsg}, nil + }, + expErr: sdkerrors.ErrUnknownRequest, + }, + "encoding error passed": { + srcRoute: capturingMessageRouter, + srcEncoder: func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + myErr := types.ErrUnpinContractFailed // any error that is not used + return nil, myErr + }, + expErr: types.ErrUnpinContractFailed, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotMsg = make([]sdk.Msg, 0) + + // when + ctx := sdk.Context{} + h := NewSDKMessageHandler(spec.srcRoute, MessageEncoders{Custom: spec.srcEncoder}) + gotEvents, gotData, gotErr := h.DispatchMsg(ctx, myContractAddr, "myPort", myContractMessage) + + // then + require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr) + if spec.expErr != nil { + require.Len(t, gotMsg, 0) + return + } + assert.Len(t, gotMsg, spec.expMsgDispatched) + for i := 0; i < spec.expMsgDispatched; i++ { + assert.Equal(t, myEvent, gotEvents[i]) + assert.Equal(t, []byte(myData), gotData[i]) + } + }) + } +} + +func TestIBCRawPacketHandler(t *testing.T) { + ibcPort := "contractsIBCPort" + var ctx sdk.Context + + var capturedPacket ibcexported.PacketI + + chanKeeper := &wasmtesting.MockChannelKeeper{ + GetNextSequenceSendFn: func(ctx sdk.Context, portID, channelID string) (uint64, bool) { + return 1, true + }, + GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channeltypes.Channel, bool) { + return channeltypes.Channel{ + Counterparty: channeltypes.NewCounterparty( + "other-port", + "other-channel-1", + ), + }, true + }, + SendPacketFn: func(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error { + capturedPacket = packet + return nil + }, + } + capKeeper := &wasmtesting.MockCapabilityKeeper{ + GetCapabilityFn: func(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) { + return &capabilitytypes.Capability{}, true + }, + } + + specs := map[string]struct { + srcMsg wasmvmtypes.SendPacketMsg + chanKeeper types.ChannelKeeper + capKeeper types.CapabilityKeeper + expPacketSent channeltypes.Packet + expErr *sdkerrors.Error + }{ + "all good": { + srcMsg: wasmvmtypes.SendPacketMsg{ + ChannelID: "channel-1", + Data: []byte("myData"), + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}}, + }, + chanKeeper: chanKeeper, + capKeeper: capKeeper, + expPacketSent: channeltypes.Packet{ + Sequence: 1, + SourcePort: ibcPort, + SourceChannel: "channel-1", + DestinationPort: "other-port", + DestinationChannel: "other-channel-1", + Data: []byte("myData"), + TimeoutHeight: clienttypes.Height{RevisionNumber: 1, RevisionHeight: 2}, + }, + }, + "sequence not found returns error": { + srcMsg: wasmvmtypes.SendPacketMsg{ + ChannelID: "channel-1", + Data: []byte("myData"), + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}}, + }, + chanKeeper: &wasmtesting.MockChannelKeeper{ + GetNextSequenceSendFn: func(ctx sdk.Context, portID, channelID string) (uint64, bool) { + return 0, false + }, + }, + expErr: channeltypes.ErrSequenceSendNotFound, + }, + "capability not found returns error": { + srcMsg: wasmvmtypes.SendPacketMsg{ + ChannelID: "channel-1", + Data: []byte("myData"), + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}}, + }, + chanKeeper: chanKeeper, + capKeeper: wasmtesting.MockCapabilityKeeper{ + GetCapabilityFn: func(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) { + return nil, false + }, + }, + expErr: channeltypes.ErrChannelCapabilityNotFound, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + capturedPacket = nil + // when + h := NewIBCRawPacketHandler(spec.chanKeeper, spec.capKeeper) + data, evts, gotErr := h.DispatchMsg(ctx, RandomAccountAddress(t), ibcPort, wasmvmtypes.CosmosMsg{IBC: &wasmvmtypes.IBCMsg{SendPacket: &spec.srcMsg}}) + // then + require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr) + if spec.expErr != nil { + return + } + assert.Nil(t, data) + assert.Nil(t, evts) + assert.Equal(t, spec.expPacketSent, capturedPacket) + }) + } +} + +func TestBurnCoinMessageHandlerIntegration(t *testing.T) { + // testing via full keeper setup so that we are confident the + // module permissions are set correct and no other handler + // picks the message in the default handler chain + ctx, keepers := CreateDefaultTestInput(t) + // set some supply + keepers.Faucet.NewFundedRandomAccount(ctx, sdk.NewCoin("denom", sdk.NewInt(10_000_000))) + k := keepers.WasmKeeper + + example := InstantiateHackatomExampleContract(t, ctx, keepers) // with deposit of 100 stake + + before, err := keepers.BankKeeper.TotalSupply(sdk.WrapSDKContext(ctx), &banktypes.QueryTotalSupplyRequest{}) + require.NoError(t, err) + + specs := map[string]struct { + msg wasmvmtypes.BurnMsg + expErr bool + }{ + "all good": { + msg: wasmvmtypes.BurnMsg{ + Amount: wasmvmtypes.Coins{{ + Denom: "denom", + Amount: "100", + }}, + }, + }, + "not enough funds in contract": { + msg: wasmvmtypes.BurnMsg{ + Amount: wasmvmtypes.Coins{{ + Denom: "denom", + Amount: "101", + }}, + }, + expErr: true, + }, + "zero amount rejected": { + msg: wasmvmtypes.BurnMsg{ + Amount: wasmvmtypes.Coins{{ + Denom: "denom", + Amount: "0", + }}, + }, + expErr: true, + }, + "unknown denom - insufficient funds": { + msg: wasmvmtypes.BurnMsg{ + Amount: wasmvmtypes.Coins{{ + Denom: "unknown", + Amount: "1", + }}, + }, + expErr: true, + }, + } + parentCtx := ctx + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx, _ = parentCtx.CacheContext() + k.wasmVM = &wasmtesting.MockWasmer{ExecuteFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{ + Messages: []wasmvmtypes.SubMsg{ + {Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{Burn: &spec.msg}}, ReplyOn: wasmvmtypes.ReplyNever}, + }, + }, 0, nil + }} + + // when + _, err = k.execute(ctx, example.Contract, example.CreatorAddr, nil, nil) + + // then + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // and total supply reduced by burned amount + after, err := keepers.BankKeeper.TotalSupply(sdk.WrapSDKContext(ctx), &banktypes.QueryTotalSupplyRequest{}) + require.NoError(t, err) + diff := before.Supply.Sub(after.Supply) + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(100))), diff) + }) + } + + // test cases: + // not enough money to burn +} diff --git a/x/wasm/keeper/ibc.go b/x/wasm/keeper/ibc.go new file mode 100644 index 00000000..62cbcc2e --- /dev/null +++ b/x/wasm/keeper/ibc.go @@ -0,0 +1,56 @@ +package keeper + +import ( + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + host "github.com/cosmos/ibc-go/v4/modules/core/24-host" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// bindIbcPort will reserve the port. +// returns a string name of the port or error if we cannot bind it. +// this will fail if call twice. +func (k Keeper) bindIbcPort(ctx sdk.Context, portID string) error { + cap := k.portKeeper.BindPort(ctx, portID) + return k.ClaimCapability(ctx, cap, host.PortPath(portID)) +} + +// ensureIbcPort is like registerIbcPort, but it checks if we already hold the port +// before calling register, so this is safe to call multiple times. +// Returns success if we already registered or just registered and error if we cannot +// (lack of permissions or someone else has it) +func (k Keeper) ensureIbcPort(ctx sdk.Context, contractAddr sdk.AccAddress) (string, error) { + portID := PortIDForContract(contractAddr) + if _, ok := k.capabilityKeeper.GetCapability(ctx, host.PortPath(portID)); ok { + return portID, nil + } + return portID, k.bindIbcPort(ctx, portID) +} + +const portIDPrefix = "wasm." + +func PortIDForContract(addr sdk.AccAddress) string { + return portIDPrefix + addr.String() +} + +func ContractFromPortID(portID string) (sdk.AccAddress, error) { + if !strings.HasPrefix(portID, portIDPrefix) { + return nil, sdkerrors.Wrapf(types.ErrInvalid, "without prefix") + } + return sdk.AccAddressFromBech32(portID[len(portIDPrefix):]) +} + +// AuthenticateCapability wraps the scopedKeeper's AuthenticateCapability function +func (k Keeper) AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) bool { + return k.capabilityKeeper.AuthenticateCapability(ctx, cap, name) +} + +// ClaimCapability allows the transfer module to claim a capability +// that IBC module passes to it +func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error { + return k.capabilityKeeper.ClaimCapability(ctx, cap, name) +} diff --git a/x/wasm/keeper/ibc_test.go b/x/wasm/keeper/ibc_test.go new file mode 100644 index 00000000..063dfb7f --- /dev/null +++ b/x/wasm/keeper/ibc_test.go @@ -0,0 +1,82 @@ +package keeper + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestDontBindPortNonIBCContract(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + example := InstantiateHackatomExampleContract(t, ctx, keepers) // ensure we bound the port + _, _, err := keepers.IBCKeeper.PortKeeper.LookupModuleByPort(ctx, keepers.WasmKeeper.GetContractInfo(ctx, example.Contract).IBCPortID) + require.Error(t, err) +} + +func TestBindingPortForIBCContractOnInstantiate(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + example := InstantiateIBCReflectContract(t, ctx, keepers) // ensure we bound the port + owner, _, err := keepers.IBCKeeper.PortKeeper.LookupModuleByPort(ctx, keepers.WasmKeeper.GetContractInfo(ctx, example.Contract).IBCPortID) + require.NoError(t, err) + require.Equal(t, "wasm", owner) + + initMsgBz := IBCReflectInitMsg{ + ReflectCodeID: example.ReflectCodeID, + }.GetBytes(t) + + // create a second contract should give yet another portID (and different address) + creator := RandomAccountAddress(t) + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, example.CodeID, creator, nil, initMsgBz, "ibc-reflect-2", nil) + require.NoError(t, err) + require.NotEqual(t, example.Contract, addr) + + portID2 := PortIDForContract(addr) + owner, _, err = keepers.IBCKeeper.PortKeeper.LookupModuleByPort(ctx, portID2) + require.NoError(t, err) + require.Equal(t, "wasm", owner) +} + +func TestContractFromPortID(t *testing.T) { + contractAddr := BuildContractAddressClassic(1, 100) + specs := map[string]struct { + srcPort string + expAddr sdk.AccAddress + expErr bool + }{ + "all good": { + srcPort: fmt.Sprintf("wasm.%s", contractAddr.String()), + expAddr: contractAddr, + }, + "without prefix": { + srcPort: contractAddr.String(), + expErr: true, + }, + "invalid prefix": { + srcPort: fmt.Sprintf("wasmx.%s", contractAddr.String()), + expErr: true, + }, + "without separator char": { + srcPort: fmt.Sprintf("wasm%s", contractAddr.String()), + expErr: true, + }, + "invalid account": { + srcPort: "wasm.foobar", + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotAddr, gotErr := ContractFromPortID(spec.srcPort) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expAddr, gotAddr) + }) + } +} diff --git a/x/wasm/keeper/keeper.go b/x/wasm/keeper/keeper.go new file mode 100644 index 00000000..98b90dea --- /dev/null +++ b/x/wasm/keeper/keeper.go @@ -0,0 +1,1188 @@ +package keeper + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "fmt" + "math" + "reflect" + "strconv" + "strings" + "time" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/cerc-io/laconicd/x/wasm/ioutils" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// contractMemoryLimit is the memory limit of each contract execution (in MiB) +// constant value so all nodes run with the same limit. +const contractMemoryLimit = 32 + +type contextKey int + +const ( + // private type creates an interface key for Context that cannot be accessed by any other package + contextKeyQueryStackSize contextKey = iota +) + +// Option is an extension point to instantiate keeper with non default values +type Option interface { + apply(*Keeper) +} + +// WasmVMQueryHandler is an extension point for custom query handler implementations +type WasmVMQueryHandler interface { + // HandleQuery executes the requested query + HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) +} + +type CoinTransferrer interface { + // TransferCoins sends the coin amounts from the source to the destination with rules applied. + TransferCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error +} + +// AccountPruner handles the balances and data cleanup for accounts that are pruned on contract instantiate. +// This is an extension point to attach custom logic +type AccountPruner interface { + // CleanupExistingAccount handles the cleanup process for balances and data of the given account. The persisted account + // type is already reset to base account at this stage. + // The method returns true when the account address can be reused. Unsupported account types are rejected by returning false + CleanupExistingAccount(ctx sdk.Context, existingAccount authtypes.AccountI) (handled bool, err error) +} + +// WasmVMResponseHandler is an extension point to handles the response data returned by a contract call. +type WasmVMResponseHandler interface { + // Handle processes the data returned by a contract invocation. + Handle( + ctx sdk.Context, + contractAddr sdk.AccAddress, + ibcPort string, + messages []wasmvmtypes.SubMsg, + origRspData []byte, + ) ([]byte, error) +} + +// list of account types that are accepted for wasm contracts. Chains importing wasmd +// can overwrite this list with the WithAcceptedAccountTypesOnContractInstantiation option. +var defaultAcceptedAccountTypes = map[reflect.Type]struct{}{ + reflect.TypeOf(&authtypes.BaseAccount{}): {}, +} + +// Keeper will have a reference to Wasmer with it's own data directory. +type Keeper struct { + storeKey sdk.StoreKey + cdc codec.Codec + accountKeeper types.AccountKeeper + bank CoinTransferrer + portKeeper types.PortKeeper + capabilityKeeper types.CapabilityKeeper + wasmVM types.WasmerEngine + wasmVMQueryHandler WasmVMQueryHandler + wasmVMResponseHandler WasmVMResponseHandler + messenger Messenger + // queryGasLimit is the max wasmvm gas that can be spent on executing a query with a contract + queryGasLimit uint64 + paramSpace paramtypes.Subspace + gasRegister GasRegister + maxQueryStackSize uint32 + acceptedAccountTypes map[reflect.Type]struct{} + accountPruner AccountPruner +} + +func (k Keeper) getUploadAccessConfig(ctx sdk.Context) types.AccessConfig { + var a types.AccessConfig + k.paramSpace.Get(ctx, types.ParamStoreKeyUploadAccess, &a) + return a +} + +func (k Keeper) getInstantiateAccessConfig(ctx sdk.Context) types.AccessType { + var a types.AccessType + k.paramSpace.Get(ctx, types.ParamStoreKeyInstantiateAccess, &a) + return a +} + +// GetParams returns the total set of wasm parameters. +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + var params types.Params + k.paramSpace.GetParamSet(ctx, ¶ms) + return params +} + +func (k Keeper) SetParams(ctx sdk.Context, ps types.Params) { + k.paramSpace.SetParamSet(ctx, &ps) +} + +func (k Keeper) create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, instantiateAccess *types.AccessConfig, authZ AuthorizationPolicy) (codeID uint64, checksum []byte, err error) { + if creator == nil { + return 0, checksum, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "cannot be nil") + } + + // figure out proper instantiate access + defaultAccessConfig := k.getInstantiateAccessConfig(ctx).With(creator) + if instantiateAccess == nil { + instantiateAccess = &defaultAccessConfig + } + chainConfigs := ChainAccessConfigs{ + Instantiate: defaultAccessConfig, + Upload: k.getUploadAccessConfig(ctx), + } + + if !authZ.CanCreateCode(chainConfigs, creator, *instantiateAccess) { + return 0, checksum, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not create code") + } + + if ioutils.IsGzip(wasmCode) { + ctx.GasMeter().ConsumeGas(k.gasRegister.UncompressCosts(len(wasmCode)), "Uncompress gzip bytecode") + wasmCode, err = ioutils.Uncompress(wasmCode, uint64(types.MaxWasmSize)) + if err != nil { + return 0, checksum, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + } + + ctx.GasMeter().ConsumeGas(k.gasRegister.CompileCosts(len(wasmCode)), "Compiling wasm bytecode") + checksum, err = k.wasmVM.Create(wasmCode) + if err != nil { + return 0, checksum, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + report, err := k.wasmVM.AnalyzeCode(checksum) + if err != nil { + return 0, checksum, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + codeID = k.autoIncrementID(ctx, types.KeyLastCodeID) + k.Logger(ctx).Debug("storing new contract", "capabilities", report.RequiredCapabilities, "code_id", codeID) + codeInfo := types.NewCodeInfo(checksum, creator, *instantiateAccess) + k.storeCodeInfo(ctx, codeID, codeInfo) + + evt := sdk.NewEvent( + types.EventTypeStoreCode, + sdk.NewAttribute(types.AttributeKeyChecksum, hex.EncodeToString(checksum)), + sdk.NewAttribute(types.AttributeKeyCodeID, strconv.FormatUint(codeID, 10)), // last element to be compatible with scripts + ) + for _, f := range strings.Split(report.RequiredCapabilities, ",") { + evt.AppendAttributes(sdk.NewAttribute(types.AttributeKeyRequiredCapability, strings.TrimSpace(f))) + } + ctx.EventManager().EmitEvent(evt) + + return codeID, checksum, nil +} + +func (k Keeper) storeCodeInfo(ctx sdk.Context, codeID uint64, codeInfo types.CodeInfo) { + store := ctx.KVStore(k.storeKey) + // 0x01 | codeID (uint64) -> ContractInfo + store.Set(types.GetCodeKey(codeID), k.cdc.MustMarshal(&codeInfo)) +} + +func (k Keeper) importCode(ctx sdk.Context, codeID uint64, codeInfo types.CodeInfo, wasmCode []byte) error { + if ioutils.IsGzip(wasmCode) { + var err error + wasmCode, err = ioutils.Uncompress(wasmCode, uint64(types.MaxWasmSize)) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + } + newCodeHash, err := k.wasmVM.Create(wasmCode) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + if !bytes.Equal(codeInfo.CodeHash, newCodeHash) { + return sdkerrors.Wrap(types.ErrInvalid, "code hashes not same") + } + + store := ctx.KVStore(k.storeKey) + key := types.GetCodeKey(codeID) + if store.Has(key) { + return sdkerrors.Wrapf(types.ErrDuplicate, "duplicate code: %d", codeID) + } + // 0x01 | codeID (uint64) -> ContractInfo + store.Set(key, k.cdc.MustMarshal(&codeInfo)) + return nil +} + +func (k Keeper) instantiate( + ctx sdk.Context, + codeID uint64, + creator, admin sdk.AccAddress, + initMsg []byte, + label string, + deposit sdk.Coins, + addressGenerator AddressGenerator, + authPolicy AuthorizationPolicy, +) (sdk.AccAddress, []byte, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "instantiate") + + if creator == nil { + return nil, nil, types.ErrEmpty.Wrap("creator") + } + instanceCosts := k.gasRegister.NewContractInstanceCosts(k.IsPinnedCode(ctx, codeID), len(initMsg)) + ctx.GasMeter().ConsumeGas(instanceCosts, "Loading CosmWasm module: instantiate") + + // get contact info + codeInfo := k.GetCodeInfo(ctx, codeID) + if codeInfo == nil { + return nil, nil, sdkerrors.Wrap(types.ErrNotFound, "code") + } + if !authPolicy.CanInstantiateContract(codeInfo.InstantiateConfig, creator) { + return nil, nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not instantiate") + } + + contractAddress := addressGenerator(ctx, codeID, codeInfo.CodeHash) + if k.HasContractInfo(ctx, contractAddress) { + return nil, nil, types.ErrDuplicate.Wrap("instance with this code id, sender and label exists: try a different label") + } + + // check account + // every cosmos module can define custom account types when needed. The cosmos-sdk comes with extension points + // to support this and a set of base and vesting account types that we integrated in our default lists. + // But not all account types of other modules are known or may make sense for contracts, therefore we kept this + // decision logic also very flexible and extendable. We provide new options to overwrite the default settings via WithAcceptedAccountTypesOnContractInstantiation and + // WithPruneAccountTypesOnContractInstantiation as constructor arguments + existingAcct := k.accountKeeper.GetAccount(ctx, contractAddress) + if existingAcct != nil { + if existingAcct.GetSequence() != 0 || existingAcct.GetPubKey() != nil { + return nil, nil, types.ErrAccountExists.Wrap("address is claimed by external account") + } + if _, accept := k.acceptedAccountTypes[reflect.TypeOf(existingAcct)]; accept { + // keep account and balance as it is + k.Logger(ctx).Info("instantiate contract with existing account", "address", contractAddress.String()) + } else { + // consider an account in the wasmd namespace spam and overwrite it. + k.Logger(ctx).Info("pruning existing account for contract instantiation", "address", contractAddress.String()) + contractAccount := k.accountKeeper.NewAccountWithAddress(ctx, contractAddress) + k.accountKeeper.SetAccount(ctx, contractAccount) + // also handle balance to not open cases where these accounts are abused and become liquid + switch handled, err := k.accountPruner.CleanupExistingAccount(ctx, existingAcct); { + case err != nil: + return nil, nil, sdkerrors.Wrap(err, "prune balance") + case !handled: + return nil, nil, types.ErrAccountExists.Wrap("address is claimed by external account") + } + } + } else { + // create an empty account (so we don't have issues later) + contractAccount := k.accountKeeper.NewAccountWithAddress(ctx, contractAddress) + k.accountKeeper.SetAccount(ctx, contractAccount) + } + // deposit initial contract funds + if !deposit.IsZero() { + if err := k.bank.TransferCoins(ctx, creator, contractAddress, deposit); err != nil { + return nil, nil, err + } + } + + // prepare params for contract instantiate call + env := types.NewEnv(ctx, contractAddress) + info := types.NewInfo(creator, deposit) + + // create prefixed data store + // 0x03 | BuildContractAddressClassic (sdk.AccAddress) + prefixStoreKey := types.GetContractStorePrefix(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + + // prepare querier + querier := k.newQueryHandler(ctx, contractAddress) + + // instantiate wasm contract + gas := k.runtimeGasForContract(ctx) + res, gasUsed, err := k.wasmVM.Instantiate(codeInfo.CodeHash, env, info, initMsg, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if err != nil { + return nil, nil, sdkerrors.Wrap(types.ErrInstantiateFailed, err.Error()) + } + + // persist instance first + createdAt := types.NewAbsoluteTxPosition(ctx) + contractInfo := types.NewContractInfo(codeID, creator, admin, label, createdAt) + + // check for IBC flag + report, err := k.wasmVM.AnalyzeCode(codeInfo.CodeHash) + if err != nil { + return nil, nil, sdkerrors.Wrap(types.ErrInstantiateFailed, err.Error()) + } + if report.HasIBCEntryPoints { + // register IBC port + ibcPort, err := k.ensureIbcPort(ctx, contractAddress) + if err != nil { + return nil, nil, err + } + contractInfo.IBCPortID = ibcPort + } + + // store contract before dispatch so that contract could be called back + historyEntry := contractInfo.InitialHistory(initMsg) + k.addToContractCodeSecondaryIndex(ctx, contractAddress, historyEntry) + k.addToContractCreatorSecondaryIndex(ctx, creator, historyEntry.Updated, contractAddress) + k.appendToContractHistory(ctx, contractAddress, historyEntry) + k.storeContractInfo(ctx, contractAddress, &contractInfo) + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeInstantiate, + sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), + sdk.NewAttribute(types.AttributeKeyCodeID, strconv.FormatUint(codeID, 10)), + )) + + data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res.Messages, res.Attributes, res.Data, res.Events) + if err != nil { + return nil, nil, sdkerrors.Wrap(err, "dispatch") + } + + return contractAddress, data, nil +} + +// Execute executes the contract instance +func (k Keeper) execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "execute") + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress) + if err != nil { + return nil, err + } + + executeCosts := k.gasRegister.InstantiateContractCosts(k.IsPinnedCode(ctx, contractInfo.CodeID), len(msg)) + ctx.GasMeter().ConsumeGas(executeCosts, "Loading CosmWasm module: execute") + + // add more funds + if !coins.IsZero() { + if err := k.bank.TransferCoins(ctx, caller, contractAddress, coins); err != nil { + return nil, err + } + } + + env := types.NewEnv(ctx, contractAddress) + info := types.NewInfo(caller, coins) + + // prepare querier + querier := k.newQueryHandler(ctx, contractAddress) + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.Execute(codeInfo.CodeHash, env, info, msg, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeExecute, + sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), + )) + + data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res.Messages, res.Attributes, res.Data, res.Events) + if err != nil { + return nil, sdkerrors.Wrap(err, "dispatch") + } + + return data, nil +} + +func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte, authZ AuthorizationPolicy) ([]byte, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "migrate") + migrateSetupCosts := k.gasRegister.InstantiateContractCosts(k.IsPinnedCode(ctx, newCodeID), len(msg)) + ctx.GasMeter().ConsumeGas(migrateSetupCosts, "Loading CosmWasm module: migrate") + + contractInfo := k.GetContractInfo(ctx, contractAddress) + if contractInfo == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") + } + if !authZ.CanModifyContract(contractInfo.AdminAddr(), caller) { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not migrate") + } + + newCodeInfo := k.GetCodeInfo(ctx, newCodeID) + if newCodeInfo == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown code") + } + + if !authZ.CanInstantiateContract(newCodeInfo.InstantiateConfig, caller) { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "to use new code") + } + + // check for IBC flag + switch report, err := k.wasmVM.AnalyzeCode(newCodeInfo.CodeHash); { + case err != nil: + return nil, sdkerrors.Wrap(types.ErrMigrationFailed, err.Error()) + case !report.HasIBCEntryPoints && contractInfo.IBCPortID != "": + // prevent update to non ibc contract + return nil, sdkerrors.Wrap(types.ErrMigrationFailed, "requires ibc callbacks") + case report.HasIBCEntryPoints && contractInfo.IBCPortID == "": + // add ibc port + ibcPort, err := k.ensureIbcPort(ctx, contractAddress) + if err != nil { + return nil, err + } + contractInfo.IBCPortID = ibcPort + } + + env := types.NewEnv(ctx, contractAddress) + + // prepare querier + querier := k.newQueryHandler(ctx, contractAddress) + + 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, cosmwasmAPI, &querier, k.gasMeter(ctx), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if err != nil { + return nil, sdkerrors.Wrap(types.ErrMigrationFailed, err.Error()) + } + // delete old secondary index entry + k.removeFromContractCodeSecondaryIndex(ctx, contractAddress, k.getLastContractHistoryEntry(ctx, contractAddress)) + // persist migration updates + historyEntry := contractInfo.AddMigration(ctx, newCodeID, msg) + k.appendToContractHistory(ctx, contractAddress, historyEntry) + k.addToContractCodeSecondaryIndex(ctx, contractAddress, historyEntry) + k.storeContractInfo(ctx, contractAddress, contractInfo) + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeMigrate, + sdk.NewAttribute(types.AttributeKeyCodeID, strconv.FormatUint(newCodeID, 10)), + sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), + )) + + data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res.Messages, res.Attributes, res.Data, res.Events) + if err != nil { + return nil, sdkerrors.Wrap(err, "dispatch") + } + + return data, nil +} + +// Sudo allows priviledged access to a contract. This can never be called by an external tx, but only by +// another native Go module directly, or on-chain governance (if sudo proposals are enabled). Thus, the keeper doesn't +// place any access controls on it, that is the responsibility or the app developer (who passes the wasm.Keeper in app.go) +func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "sudo") + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress) + if err != nil { + return nil, err + } + + sudoSetupCosts := k.gasRegister.InstantiateContractCosts(k.IsPinnedCode(ctx, contractInfo.CodeID), len(msg)) + ctx.GasMeter().ConsumeGas(sudoSetupCosts, "Loading CosmWasm module: sudo") + + env := types.NewEnv(ctx, contractAddress) + + // prepare querier + querier := k.newQueryHandler(ctx, contractAddress) + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.Sudo(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeSudo, + sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), + )) + + data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res.Messages, res.Attributes, res.Data, res.Events) + if err != nil { + return nil, sdkerrors.Wrap(err, "dispatch") + } + + return data, nil +} + +// reply is only called from keeper internal functions (dispatchSubmessages) after processing the submessage +func (k Keeper) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress) + if err != nil { + return nil, err + } + + // always consider this pinned + replyCosts := k.gasRegister.ReplyCosts(true, reply) + ctx.GasMeter().ConsumeGas(replyCosts, "Loading CosmWasm module: reply") + + env := types.NewEnv(ctx, contractAddress) + + // prepare querier + querier := k.newQueryHandler(ctx, contractAddress) + gas := k.runtimeGasForContract(ctx) + + res, gasUsed, execErr := k.wasmVM.Reply(codeInfo.CodeHash, env, reply, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeReply, + sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), + )) + + data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res.Messages, res.Attributes, res.Data, res.Events) + if err != nil { + return nil, sdkerrors.Wrap(err, "dispatch") + } + + return data, nil +} + +// addToContractCodeSecondaryIndex adds element to the index for contracts-by-codeid queries +func (k Keeper) addToContractCodeSecondaryIndex(ctx sdk.Context, contractAddress sdk.AccAddress, entry types.ContractCodeHistoryEntry) { + store := ctx.KVStore(k.storeKey) + store.Set(types.GetContractByCreatedSecondaryIndexKey(contractAddress, entry), []byte{}) +} + +// removeFromContractCodeSecondaryIndex removes element to the index for contracts-by-codeid queries +func (k Keeper) removeFromContractCodeSecondaryIndex(ctx sdk.Context, contractAddress sdk.AccAddress, entry types.ContractCodeHistoryEntry) { + ctx.KVStore(k.storeKey).Delete(types.GetContractByCreatedSecondaryIndexKey(contractAddress, entry)) +} + +// addToContractCreatorSecondaryIndex adds element to the index for contracts-by-creator queries +func (k Keeper) addToContractCreatorSecondaryIndex(ctx sdk.Context, creatorAddress sdk.AccAddress, position *types.AbsoluteTxPosition, contractAddress sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + store.Set(types.GetContractByCreatorSecondaryIndexKey(creatorAddress, position.Bytes(), contractAddress), []byte{}) +} + +// IterateContractsByCreator iterates over all contracts with given creator address in order of creation time asc. +func (k Keeper) IterateContractsByCreator(ctx sdk.Context, creator sdk.AccAddress, cb func(address sdk.AccAddress) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.GetContractsByCreatorPrefix(creator)) + for iter := prefixStore.Iterator(nil, nil); iter.Valid(); iter.Next() { + key := iter.Key() + if cb(key[types.AbsoluteTxPositionLen:]) { + return + } + } +} + +// IterateContractsByCode iterates over all contracts with given codeID ASC on code update time. +func (k Keeper) IterateContractsByCode(ctx sdk.Context, codeID uint64, cb func(address sdk.AccAddress) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.GetContractByCodeIDSecondaryIndexPrefix(codeID)) + iter := prefixStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + key := iter.Key() + if cb(key[types.AbsoluteTxPositionLen:]) { + return + } + } +} + +func (k Keeper) setContractAdmin(ctx sdk.Context, contractAddress, caller, newAdmin sdk.AccAddress, authZ AuthorizationPolicy) error { + contractInfo := k.GetContractInfo(ctx, contractAddress) + if contractInfo == nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") + } + if !authZ.CanModifyContract(contractInfo.AdminAddr(), caller) { + return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not modify contract") + } + newAdminStr := newAdmin.String() + contractInfo.Admin = newAdminStr + k.storeContractInfo(ctx, contractAddress, contractInfo) + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeUpdateContractAdmin, + sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), + sdk.NewAttribute(types.AttributeKeyNewAdmin, newAdminStr), + )) + + return nil +} + +func (k Keeper) appendToContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress, newEntries ...types.ContractCodeHistoryEntry) { + store := ctx.KVStore(k.storeKey) + // find last element position + var pos uint64 + prefixStore := prefix.NewStore(store, types.GetContractCodeHistoryElementPrefix(contractAddr)) + iter := prefixStore.ReverseIterator(nil, nil) + defer iter.Close() + + if iter.Valid() { + pos = sdk.BigEndianToUint64(iter.Key()) + } + // then store with incrementing position + for _, e := range newEntries { + pos++ + key := types.GetContractCodeHistoryElementKey(contractAddr, pos) + store.Set(key, k.cdc.MustMarshal(&e)) //nolint:gosec + } +} + +func (k Keeper) GetContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress) []types.ContractCodeHistoryEntry { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.GetContractCodeHistoryElementPrefix(contractAddr)) + r := make([]types.ContractCodeHistoryEntry, 0) + iter := prefixStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + var e types.ContractCodeHistoryEntry + k.cdc.MustUnmarshal(iter.Value(), &e) + r = append(r, e) + } + return r +} + +// getLastContractHistoryEntry returns the last element from history. To be used internally only as it panics when none exists +func (k Keeper) getLastContractHistoryEntry(ctx sdk.Context, contractAddr sdk.AccAddress) types.ContractCodeHistoryEntry { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.GetContractCodeHistoryElementPrefix(contractAddr)) + iter := prefixStore.ReverseIterator(nil, nil) + defer iter.Close() + + var r types.ContractCodeHistoryEntry + if !iter.Valid() { + // all contracts have a history + panic(fmt.Sprintf("no history for %s", contractAddr.String())) + } + k.cdc.MustUnmarshal(iter.Value(), &r) + return r +} + +// QuerySmart queries the smart contract itself. +func (k Keeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "query-smart") + + // checks and increase query stack size + ctx, err := checkAndIncreaseQueryStackSize(ctx, k.maxQueryStackSize) + if err != nil { + return nil, err + } + + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return nil, err + } + + smartQuerySetupCosts := k.gasRegister.InstantiateContractCosts(k.IsPinnedCode(ctx, contractInfo.CodeID), len(req)) + ctx.GasMeter().ConsumeGas(smartQuerySetupCosts, "Loading CosmWasm module: query") + + // prepare querier + querier := k.newQueryHandler(ctx, contractAddr) + + env := types.NewEnv(ctx, contractAddr) + queryResult, gasUsed, qErr := k.wasmVM.Query(codeInfo.CodeHash, env, req, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), k.runtimeGasForContract(ctx), costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if qErr != nil { + return nil, sdkerrors.Wrap(types.ErrQueryFailed, qErr.Error()) + } + return queryResult, nil +} + +func checkAndIncreaseQueryStackSize(ctx sdk.Context, maxQueryStackSize uint32) (sdk.Context, error) { + var queryStackSize uint32 + + // read current value + if size := ctx.Context().Value(contextKeyQueryStackSize); size != nil { + queryStackSize = size.(uint32) + } else { + queryStackSize = 0 + } + + // increase + queryStackSize++ + + // did we go too far? + if queryStackSize > maxQueryStackSize { + return ctx, types.ErrExceedMaxQueryStackSize + } + + // set updated stack size + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contextKeyQueryStackSize, queryStackSize)) + + return ctx, nil +} + +// QueryRaw returns the contract's state for give key. Returns `nil` when key is `nil`. +func (k Keeper) QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "query-raw") + if key == nil { + return nil + } + prefixStoreKey := types.GetContractStorePrefix(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + return prefixStore.Get(key) +} + +func (k Keeper) contractInstance(ctx sdk.Context, contractAddress sdk.AccAddress) (types.ContractInfo, types.CodeInfo, prefix.Store, error) { + store := ctx.KVStore(k.storeKey) + + contractBz := store.Get(types.GetContractAddressKey(contractAddress)) + if contractBz == nil { + return types.ContractInfo{}, types.CodeInfo{}, prefix.Store{}, sdkerrors.Wrap(types.ErrNotFound, "contract") + } + var contractInfo types.ContractInfo + k.cdc.MustUnmarshal(contractBz, &contractInfo) + + codeInfoBz := store.Get(types.GetCodeKey(contractInfo.CodeID)) + if codeInfoBz == nil { + return contractInfo, types.CodeInfo{}, prefix.Store{}, sdkerrors.Wrap(types.ErrNotFound, "code info") + } + var codeInfo types.CodeInfo + k.cdc.MustUnmarshal(codeInfoBz, &codeInfo) + prefixStoreKey := types.GetContractStorePrefix(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + return contractInfo, codeInfo, prefixStore, nil +} + +func (k Keeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + store := ctx.KVStore(k.storeKey) + var contract types.ContractInfo + contractBz := store.Get(types.GetContractAddressKey(contractAddress)) + if contractBz == nil { + return nil + } + k.cdc.MustUnmarshal(contractBz, &contract) + return &contract +} + +func (k Keeper) HasContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.GetContractAddressKey(contractAddress)) +} + +// storeContractInfo persists the ContractInfo. No secondary index updated here. +func (k Keeper) storeContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress, contract *types.ContractInfo) { + store := ctx.KVStore(k.storeKey) + store.Set(types.GetContractAddressKey(contractAddress), k.cdc.MustMarshal(contract)) +} + +func (k Keeper) IterateContractInfo(ctx sdk.Context, cb func(sdk.AccAddress, types.ContractInfo) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ContractKeyPrefix) + iter := prefixStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + var contract types.ContractInfo + k.cdc.MustUnmarshal(iter.Value(), &contract) + // cb returns true to stop early + if cb(iter.Key(), contract) { + break + } + } +} + +// IterateContractState iterates through all elements of the key value store for the given contract address and passes +// them to the provided callback function. The callback method can return true to abort early. +func (k Keeper) IterateContractState(ctx sdk.Context, contractAddress sdk.AccAddress, cb func(key, value []byte) bool) { + prefixStoreKey := types.GetContractStorePrefix(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + iter := prefixStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + if cb(iter.Key(), iter.Value()) { + break + } + } +} + +func (k Keeper) importContractState(ctx sdk.Context, contractAddress sdk.AccAddress, models []types.Model) error { + prefixStoreKey := types.GetContractStorePrefix(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + for _, model := range models { + if model.Value == nil { + model.Value = []byte{} + } + if prefixStore.Has(model.Key) { + return sdkerrors.Wrapf(types.ErrDuplicate, "duplicate key: %x", model.Key) + } + prefixStore.Set(model.Key, model.Value) + } + return 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.MustUnmarshal(codeInfoBz, &codeInfo) + return &codeInfo +} + +func (k Keeper) containsCodeInfo(ctx sdk.Context, codeID uint64) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.GetCodeKey(codeID)) +} + +func (k Keeper) IterateCodeInfos(ctx sdk.Context, cb func(uint64, types.CodeInfo) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.CodeKeyPrefix) + iter := prefixStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + var c types.CodeInfo + k.cdc.MustUnmarshal(iter.Value(), &c) + // cb returns true to stop early + if cb(binary.BigEndian.Uint64(iter.Key()), c) { + return + } + } +} + +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.MustUnmarshal(codeInfoBz, &codeInfo) + return k.wasmVM.GetCode(codeInfo.CodeHash) +} + +// PinCode pins the wasm contract in wasmvm cache +func (k Keeper) pinCode(ctx sdk.Context, codeID uint64) error { + codeInfo := k.GetCodeInfo(ctx, codeID) + if codeInfo == nil { + return sdkerrors.Wrap(types.ErrNotFound, "code info") + } + + if err := k.wasmVM.Pin(codeInfo.CodeHash); err != nil { + return sdkerrors.Wrap(types.ErrPinContractFailed, err.Error()) + } + store := ctx.KVStore(k.storeKey) + // store 1 byte to not run into `nil` debugging issues + store.Set(types.GetPinnedCodeIndexPrefix(codeID), []byte{1}) + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypePinCode, + sdk.NewAttribute(types.AttributeKeyCodeID, strconv.FormatUint(codeID, 10)), + )) + return nil +} + +// UnpinCode removes the wasm contract from wasmvm cache +func (k Keeper) unpinCode(ctx sdk.Context, codeID uint64) error { + codeInfo := k.GetCodeInfo(ctx, codeID) + if codeInfo == nil { + return sdkerrors.Wrap(types.ErrNotFound, "code info") + } + if err := k.wasmVM.Unpin(codeInfo.CodeHash); err != nil { + return sdkerrors.Wrap(types.ErrUnpinContractFailed, err.Error()) + } + + store := ctx.KVStore(k.storeKey) + store.Delete(types.GetPinnedCodeIndexPrefix(codeID)) + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeUnpinCode, + sdk.NewAttribute(types.AttributeKeyCodeID, strconv.FormatUint(codeID, 10)), + )) + return nil +} + +// IsPinnedCode returns true when codeID is pinned in wasmvm cache +func (k Keeper) IsPinnedCode(ctx sdk.Context, codeID uint64) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.GetPinnedCodeIndexPrefix(codeID)) +} + +// InitializePinnedCodes updates wasmvm to pin to cache all contracts marked as pinned +func (k Keeper) InitializePinnedCodes(ctx sdk.Context) error { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.PinnedCodeIndexPrefix) + iter := store.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + codeInfo := k.GetCodeInfo(ctx, types.ParsePinnedCodeIndex(iter.Key())) + if codeInfo == nil { + return sdkerrors.Wrap(types.ErrNotFound, "code info") + } + if err := k.wasmVM.Pin(codeInfo.CodeHash); err != nil { + return sdkerrors.Wrap(types.ErrPinContractFailed, err.Error()) + } + } + return nil +} + +// setContractInfoExtension updates the extension point data that is stored with the contract info +func (k Keeper) setContractInfoExtension(ctx sdk.Context, contractAddr sdk.AccAddress, ext types.ContractInfoExtension) error { + info := k.GetContractInfo(ctx, contractAddr) + if info == nil { + return sdkerrors.Wrap(types.ErrNotFound, "contract info") + } + if err := info.SetExtension(ext); err != nil { + return err + } + k.storeContractInfo(ctx, contractAddr, info) + return nil +} + +// setAccessConfig updates the access config of a code id. +func (k Keeper) setAccessConfig(ctx sdk.Context, codeID uint64, caller sdk.AccAddress, newConfig types.AccessConfig, authz AuthorizationPolicy) error { + info := k.GetCodeInfo(ctx, codeID) + if info == nil { + return sdkerrors.Wrap(types.ErrNotFound, "code info") + } + isSubset := newConfig.Permission.IsSubset(k.getInstantiateAccessConfig(ctx)) + if !authz.CanModifyCodeAccessConfig(sdk.MustAccAddressFromBech32(info.Creator), caller, isSubset) { + return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not modify code access config") + } + + info.InstantiateConfig = newConfig + k.storeCodeInfo(ctx, codeID, *info) + evt := sdk.NewEvent( + types.EventTypeUpdateCodeAccessConfig, + sdk.NewAttribute(types.AttributeKeyCodePermission, newConfig.Permission.String()), + sdk.NewAttribute(types.AttributeKeyCodeID, strconv.FormatUint(codeID, 10)), + ) + if addrs := newConfig.AllAuthorizedAddresses(); len(addrs) != 0 { + attr := sdk.NewAttribute(types.AttributeKeyAuthorizedAddresses, strings.Join(addrs, ",")) + evt.Attributes = append(evt.Attributes, attr.ToKVPair()) + } + ctx.EventManager().EmitEvent(evt) + return nil +} + +// handleContractResponse processes the contract response data by emitting events and sending sub-/messages. +func (k *Keeper) handleContractResponse( + ctx sdk.Context, + contractAddr sdk.AccAddress, + ibcPort string, + msgs []wasmvmtypes.SubMsg, + attrs []wasmvmtypes.EventAttribute, + data []byte, + evts wasmvmtypes.Events, +) ([]byte, error) { + attributeGasCost := k.gasRegister.EventCosts(attrs, evts) + ctx.GasMeter().ConsumeGas(attributeGasCost, "Custom contract event attributes") + // emit all events from this contract itself + if len(attrs) != 0 { + wasmEvents, err := newWasmModuleEvent(attrs, contractAddr) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(wasmEvents) + } + if len(evts) > 0 { + customEvents, err := newCustomEvents(evts, contractAddr) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(customEvents) + } + return k.wasmVMResponseHandler.Handle(ctx, contractAddr, ibcPort, msgs, data) +} + +func (k Keeper) runtimeGasForContract(ctx sdk.Context) uint64 { + meter := ctx.GasMeter() + if meter.IsOutOfGas() { + return 0 + } + if meter.Limit() == 0 { // infinite gas meter with limit=0 and not out of gas + return math.MaxUint64 + } + return k.gasRegister.ToWasmVMGas(meter.Limit() - meter.GasConsumedToLimit()) +} + +func (k Keeper) consumeRuntimeGas(ctx sdk.Context, gas uint64) { + consumed := k.gasRegister.FromWasmVMGas(gas) + ctx.GasMeter().ConsumeGas(consumed, "wasm contract") + // throw OutOfGas error if we ran out (got exactly to zero due to better limit enforcing) + if ctx.GasMeter().IsOutOfGas() { + panic(sdk.ErrorOutOfGas{Descriptor: "Wasmer function execution"}) + } +} + +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 +} + +// PeekAutoIncrementID reads the current value without incrementing it. +func (k Keeper) PeekAutoIncrementID(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) + } + return id +} + +func (k Keeper) importAutoIncrementID(ctx sdk.Context, lastIDKey []byte, val uint64) error { + store := ctx.KVStore(k.storeKey) + if store.Has(lastIDKey) { + return sdkerrors.Wrapf(types.ErrDuplicate, "autoincrement id: %s", string(lastIDKey)) + } + bz := sdk.Uint64ToBigEndian(val) + store.Set(lastIDKey, bz) + return nil +} + +func (k Keeper) importContract(ctx sdk.Context, contractAddr sdk.AccAddress, c *types.ContractInfo, state []types.Model, entries []types.ContractCodeHistoryEntry) error { + if !k.containsCodeInfo(ctx, c.CodeID) { + return sdkerrors.Wrapf(types.ErrNotFound, "code id: %d", c.CodeID) + } + if k.HasContractInfo(ctx, contractAddr) { + return sdkerrors.Wrapf(types.ErrDuplicate, "contract: %s", contractAddr) + } + + creatorAddress, err := sdk.AccAddressFromBech32(c.Creator) + if err != nil { + return err + } + + k.appendToContractHistory(ctx, contractAddr, entries...) + k.storeContractInfo(ctx, contractAddr, c) + k.addToContractCodeSecondaryIndex(ctx, contractAddr, entries[len(entries)-1]) + k.addToContractCreatorSecondaryIndex(ctx, creatorAddress, entries[0].Updated, contractAddr) + return k.importContractState(ctx, contractAddr, state) +} + +func (k Keeper) newQueryHandler(ctx sdk.Context, contractAddress sdk.AccAddress) QueryHandler { + return NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddress, k.gasRegister) +} + +// MultipliedGasMeter wraps the GasMeter from context and multiplies all reads by out defined multiplier +type MultipliedGasMeter struct { + originalMeter sdk.GasMeter + GasRegister GasRegister +} + +func NewMultipliedGasMeter(originalMeter sdk.GasMeter, gr GasRegister) MultipliedGasMeter { + return MultipliedGasMeter{originalMeter: originalMeter, GasRegister: gr} +} + +var _ wasmvm.GasMeter = MultipliedGasMeter{} + +func (m MultipliedGasMeter) GasConsumed() sdk.Gas { + return m.GasRegister.ToWasmVMGas(m.originalMeter.GasConsumed()) +} + +func (k Keeper) gasMeter(ctx sdk.Context) MultipliedGasMeter { + return NewMultipliedGasMeter(ctx.GasMeter(), k.gasRegister) +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return moduleLogger(ctx) +} + +func moduleLogger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +// Querier creates a new grpc querier instance +func Querier(k *Keeper) *grpcQuerier { //nolint:revive + return NewGrpcQuerier(k.cdc, k.storeKey, k, k.queryGasLimit) +} + +// QueryGasLimit returns the gas limit for smart queries. +func (k Keeper) QueryGasLimit() sdk.Gas { + return k.queryGasLimit +} + +// BankCoinTransferrer replicates the cosmos-sdk behaviour as in +// https://github.com/cosmos/cosmos-sdk/blob/v0.41.4/x/bank/keeper/msg_server.go#L26 +type BankCoinTransferrer struct { + keeper types.BankKeeper +} + +func NewBankCoinTransferrer(keeper types.BankKeeper) BankCoinTransferrer { + return BankCoinTransferrer{ + keeper: keeper, + } +} + +// TransferCoins transfers coins from source to destination account when coin send was enabled for them and the recipient +// is not in the blocked address list. +func (c BankCoinTransferrer) TransferCoins(parentCtx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amount sdk.Coins) error { + em := sdk.NewEventManager() + ctx := parentCtx.WithEventManager(em) + if err := c.keeper.IsSendEnabledCoins(ctx, amount...); err != nil { + return err + } + if c.keeper.BlockedAddr(toAddr) { + return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", toAddr.String()) + } + + sdkerr := c.keeper.SendCoins(ctx, fromAddr, toAddr, amount) + if sdkerr != nil { + return sdkerr + } + for _, e := range em.Events() { + if e.Type == sdk.EventTypeMessage { // skip messages as we talk to the keeper directly + continue + } + parentCtx.EventManager().EmitEvent(e) + } + return nil +} + +var _ AccountPruner = VestingCoinBurner{} + +// VestingCoinBurner default implementation for AccountPruner to burn the coins +type VestingCoinBurner struct { + bank types.BankKeeper +} + +// NewVestingCoinBurner constructor +func NewVestingCoinBurner(bank types.BankKeeper) VestingCoinBurner { + if bank == nil { + panic("bank keeper must not be nil") + } + return VestingCoinBurner{bank: bank} +} + +// CleanupExistingAccount accepts only vesting account types to burns all their original vesting coin balances. +// Other account types will be rejected and returned as unhandled. +func (b VestingCoinBurner) CleanupExistingAccount(ctx sdk.Context, existingAcc authtypes.AccountI) (handled bool, err error) { + v, ok := existingAcc.(vestingexported.VestingAccount) + if !ok { + return false, nil + } + + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + coinsToBurn := sdk.NewCoins() + for _, orig := range v.GetOriginalVesting() { // focus on the coin denoms that were setup originally; getAllBalances has some issues + coinsToBurn = append(coinsToBurn, b.bank.GetBalance(ctx, existingAcc.GetAddress(), orig.Denom)) + } + if err := b.bank.SendCoinsFromAccountToModule(ctx, existingAcc.GetAddress(), types.ModuleName, coinsToBurn); err != nil { + return false, sdkerrors.Wrap(err, "prune account balance") + } + if err := b.bank.BurnCoins(ctx, types.ModuleName, coinsToBurn); err != nil { + return false, sdkerrors.Wrap(err, "burn account balance") + } + return true, nil +} + +type msgDispatcher interface { + DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) +} + +// DefaultWasmVMContractResponseHandler default implementation that first dispatches submessage then normal messages. +// The Submessage execution may include an success/failure response handling by the contract that can overwrite the +// original +type DefaultWasmVMContractResponseHandler struct { + md msgDispatcher +} + +func NewDefaultWasmVMContractResponseHandler(md msgDispatcher) *DefaultWasmVMContractResponseHandler { + return &DefaultWasmVMContractResponseHandler{md: md} +} + +// Handle processes the data returned by a contract invocation. +func (h DefaultWasmVMContractResponseHandler) Handle(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, messages []wasmvmtypes.SubMsg, origRspData []byte) ([]byte, error) { + result := origRspData + switch rsp, err := h.md.DispatchSubmessages(ctx, contractAddr, ibcPort, messages); { + case err != nil: + return nil, sdkerrors.Wrap(err, "submessages") + case rsp != nil: + result = rsp + } + return result, nil +} diff --git a/x/wasm/keeper/keeper_cgo.go b/x/wasm/keeper/keeper_cgo.go new file mode 100644 index 00000000..3eea2177 --- /dev/null +++ b/x/wasm/keeper/keeper_cgo.go @@ -0,0 +1,69 @@ +//go:build cgo + +package keeper + +import ( + "path/filepath" + + wasmvm "github.com/CosmWasm/wasmvm" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// NewKeeper creates a new contract Keeper instance +// If customEncoders is non-nil, we can use this to override some of the message handler, especially custom +func NewKeeper( + cdc codec.Codec, + storeKey sdk.StoreKey, + paramSpace paramtypes.Subspace, + accountKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, + stakingKeeper types.StakingKeeper, + distKeeper types.DistributionKeeper, + channelKeeper types.ChannelKeeper, + portKeeper types.PortKeeper, + capabilityKeeper types.CapabilityKeeper, + portSource types.ICS20TransferPortSource, + router MessageRouter, + queryRouter GRPCQueryRouter, + homeDir string, + wasmConfig types.WasmConfig, + availableCapabilities string, + opts ...Option, +) Keeper { + wasmer, err := wasmvm.NewVM(filepath.Join(homeDir, "wasm"), availableCapabilities, contractMemoryLimit, wasmConfig.ContractDebugMode, wasmConfig.MemoryCacheSize) + if err != nil { + panic(err) + } + // set KeyTable if it has not already been set + if !paramSpace.HasKeyTable() { + paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable()) + } + + keeper := &Keeper{ + storeKey: storeKey, + cdc: cdc, + wasmVM: wasmer, + accountKeeper: accountKeeper, + bank: NewBankCoinTransferrer(bankKeeper), + accountPruner: NewVestingCoinBurner(bankKeeper), + portKeeper: portKeeper, + capabilityKeeper: capabilityKeeper, + messenger: NewDefaultMessageHandler(router, channelKeeper, capabilityKeeper, bankKeeper, cdc, portSource), + queryGasLimit: wasmConfig.SmartQueryGasLimit, + paramSpace: paramSpace, + gasRegister: NewDefaultWasmGasRegister(), + maxQueryStackSize: types.DefaultMaxQueryStackSize, + acceptedAccountTypes: defaultAcceptedAccountTypes, + } + keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, keeper) + for _, o := range opts { + o.apply(keeper) + } + // not updateable, yet + keeper.wasmVMResponseHandler = NewDefaultWasmVMContractResponseHandler(NewMessageDispatcher(keeper.messenger, keeper)) + return *keeper +} diff --git a/x/wasm/keeper/keeper_no_cgo.go b/x/wasm/keeper/keeper_no_cgo.go new file mode 100644 index 00000000..f05ff0b7 --- /dev/null +++ b/x/wasm/keeper/keeper_no_cgo.go @@ -0,0 +1,35 @@ +//go:build !cgo + +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// NewKeeper creates a new contract Keeper instance +// If customEncoders is non-nil, we can use this to override some of the message handler, especially custom +func NewKeeper( + cdc codec.Codec, + storeKey sdk.StoreKey, + paramSpace paramtypes.Subspace, + accountKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, + stakingKeeper types.StakingKeeper, + distKeeper types.DistributionKeeper, + channelKeeper types.ChannelKeeper, + portKeeper types.PortKeeper, + capabilityKeeper types.CapabilityKeeper, + portSource types.ICS20TransferPortSource, + router MessageRouter, + queryRouter GRPCQueryRouter, + homeDir string, + wasmConfig types.WasmConfig, + availableCapabilities string, + opts ...Option, +) Keeper { + panic("not implemented, please build with cgo enabled") +} diff --git a/x/wasm/keeper/keeper_test.go b/x/wasm/keeper/keeper_test.go new file mode 100644 index 00000000..951a6a8c --- /dev/null +++ b/x/wasm/keeper/keeper_test.go @@ -0,0 +1,2410 @@ +package keeper + +import ( + "bytes" + _ "embed" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "testing" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + stypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/rand" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +//go:embed testdata/hackatom.wasm +var hackatomWasm []byte + +const AvailableCapabilities = "iterator,staking,stargate,cosmwasm_1_1" + +func TestNewKeeper(t *testing.T) { + _, keepers := CreateTestInput(t, false, AvailableCapabilities) + require.NotNil(t, keepers.ContractKeeper) +} + +func TestCreateSuccess(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + em := sdk.NewEventManager() + contractID, _, err := keeper.Create(ctx.WithEventManager(em), creator, hackatomWasm, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + // and verify content + storedCode, err := keepers.WasmKeeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, hackatomWasm, storedCode) + // and events emitted + codeHash := strings.ToLower("beb3de5e9b93b52e514c74ce87ccddb594b9bcd33b7f1af1bb6da63fc883917b") + exp := sdk.Events{sdk.NewEvent("store_code", sdk.NewAttribute("code_checksum", codeHash), sdk.NewAttribute("code_id", "1"))} + assert.Equal(t, exp, em.Events()) +} + +func TestCreateNilCreatorAddress(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + + _, _, err := keepers.ContractKeeper.Create(ctx, nil, hackatomWasm, nil) + require.Error(t, err, "nil creator is not allowed") +} + +func TestCreateNilWasmCode(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + _, _, err := keepers.ContractKeeper.Create(ctx, creator, nil, nil) + require.Error(t, err, "nil WASM code is not allowed") +} + +func TestCreateInvalidWasmCode(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + _, _, err := keepers.ContractKeeper.Create(ctx, creator, []byte("potatoes"), nil) + require.Error(t, err, "potatoes are not valid WASM code") +} + +func TestCreateStoresInstantiatePermission(t *testing.T) { + var ( + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + myAddr sdk.AccAddress = bytes.Repeat([]byte{1}, types.SDKAddrLen) + ) + + specs := map[string]struct { + srcPermission types.AccessType + expInstConf types.AccessConfig + }{ + "default": { + srcPermission: types.DefaultParams().InstantiateDefaultPermission, + expInstConf: types.AllowEverybody, + }, + "everybody": { + srcPermission: types.AccessTypeEverybody, + expInstConf: types.AllowEverybody, + }, + "nobody": { + srcPermission: types.AccessTypeNobody, + expInstConf: types.AllowNobody, + }, + "onlyAddress with matching address": { + srcPermission: types.AccessTypeOnlyAddress, + expInstConf: types.AccessConfig{Permission: types.AccessTypeOnlyAddress, Address: myAddr.String()}, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper + keepers.WasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowEverybody, + InstantiateDefaultPermission: spec.srcPermission, + }) + fundAccounts(t, ctx, accKeeper, bankKeeper, myAddr, deposit) + + codeID, _, err := keeper.Create(ctx, myAddr, hackatomWasm, nil) + require.NoError(t, err) + + codeInfo := keepers.WasmKeeper.GetCodeInfo(ctx, codeID) + require.NotNil(t, codeInfo) + assert.True(t, spec.expInstConf.Equals(codeInfo.InstantiateConfig), "got %#v", codeInfo.InstantiateConfig) + }) + } +} + +func TestCreateWithParamPermissions(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + otherAddr := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + specs := map[string]struct { + policy AuthorizationPolicy + chainUpload types.AccessConfig + expError *sdkerrors.Error + }{ + "default": { + policy: DefaultAuthorizationPolicy{}, + chainUpload: types.DefaultUploadAccess, + }, + "everybody": { + policy: DefaultAuthorizationPolicy{}, + chainUpload: types.AllowEverybody, + }, + "nobody": { + policy: DefaultAuthorizationPolicy{}, + chainUpload: types.AllowNobody, + expError: sdkerrors.ErrUnauthorized, + }, + "onlyAddress with matching address": { + policy: DefaultAuthorizationPolicy{}, + chainUpload: types.AccessTypeOnlyAddress.With(creator), + }, + "onlyAddress with non matching address": { + policy: DefaultAuthorizationPolicy{}, + chainUpload: types.AccessTypeOnlyAddress.With(otherAddr), + expError: sdkerrors.ErrUnauthorized, + }, + "gov: always allowed": { + policy: GovAuthorizationPolicy{}, + chainUpload: types.AllowNobody, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + params := types.DefaultParams() + params.CodeUploadAccess = spec.chainUpload + keepers.WasmKeeper.SetParams(ctx, params) + keeper := NewPermissionedKeeper(keepers.WasmKeeper, spec.policy) + _, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.True(t, spec.expError.Is(err), err) + if spec.expError != nil { + return + } + }) + } +} + +// ensure that the user cannot set the code instantiate permission to something more permissive +// than the default +func TestEnforceValidPermissionsOnCreate(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + contractKeeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + other := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + onlyCreator := types.AccessTypeOnlyAddress.With(creator) + onlyOther := types.AccessTypeOnlyAddress.With(other) + + specs := map[string]struct { + defaultPermssion types.AccessType + requestedPermission *types.AccessConfig + // grantedPermission is set iff no error + grantedPermission types.AccessConfig + // expError is nil iff the request is allowed + expError *sdkerrors.Error + }{ + "override everybody": { + defaultPermssion: types.AccessTypeEverybody, + requestedPermission: &onlyCreator, + grantedPermission: onlyCreator, + }, + "default to everybody": { + defaultPermssion: types.AccessTypeEverybody, + requestedPermission: nil, + grantedPermission: types.AccessConfig{Permission: types.AccessTypeEverybody}, + }, + "explicitly set everybody": { + defaultPermssion: types.AccessTypeEverybody, + requestedPermission: &types.AccessConfig{Permission: types.AccessTypeEverybody}, + grantedPermission: types.AccessConfig{Permission: types.AccessTypeEverybody}, + }, + "cannot override nobody": { + defaultPermssion: types.AccessTypeNobody, + requestedPermission: &onlyCreator, + expError: sdkerrors.ErrUnauthorized, + }, + "default to nobody": { + defaultPermssion: types.AccessTypeNobody, + requestedPermission: nil, + grantedPermission: types.AccessConfig{Permission: types.AccessTypeNobody}, + }, + "only defaults to code creator": { + defaultPermssion: types.AccessTypeOnlyAddress, + requestedPermission: nil, + grantedPermission: onlyCreator, + }, + "can explicitly set to code creator": { + defaultPermssion: types.AccessTypeOnlyAddress, + requestedPermission: &onlyCreator, + grantedPermission: onlyCreator, + }, + "cannot override which address in only": { + defaultPermssion: types.AccessTypeOnlyAddress, + requestedPermission: &onlyOther, + expError: sdkerrors.ErrUnauthorized, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + params := types.DefaultParams() + params.InstantiateDefaultPermission = spec.defaultPermssion + keeper.SetParams(ctx, params) + codeID, _, err := contractKeeper.Create(ctx, creator, hackatomWasm, spec.requestedPermission) + require.True(t, spec.expError.Is(err), err) + if spec.expError == nil { + codeInfo := keeper.GetCodeInfo(ctx, codeID) + require.Equal(t, codeInfo.InstantiateConfig, spec.grantedPermission) + } + }) + } +} + +func TestCreateDuplicate(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // create one copy + contractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + + // create second copy + duplicateID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + require.Equal(t, uint64(2), duplicateID) + + // and verify both content is proper + storedCode, err := keepers.WasmKeeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, hackatomWasm, storedCode) + storedCode, err = keepers.WasmKeeper.GetByteCode(ctx, duplicateID) + require.NoError(t, err) + require.Equal(t, hackatomWasm, storedCode) +} + +func TestCreateWithSimulation(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + + ctx = ctx.WithBlockHeader(tmproto.Header{Height: 1}). + WithGasMeter(stypes.NewInfiniteGasMeter()) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // create this once in simulation mode + contractID, _, err := keepers.ContractKeeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + + // then try to create it in non-simulation mode (should not fail) + ctx, keepers = CreateTestInput(t, false, AvailableCapabilities) + ctx = ctx.WithGasMeter(sdk.NewGasMeter(10_000_000)) + creator = keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + contractID, _, err = keepers.ContractKeeper.Create(ctx, creator, hackatomWasm, nil) + + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + + // and verify content + code, err := keepers.WasmKeeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, code, hackatomWasm) +} + +func TestIsSimulationMode(t *testing.T) { + specs := map[string]struct { + ctx sdk.Context + exp bool + }{ + "genesis block": { + ctx: sdk.Context{}.WithBlockHeader(tmproto.Header{}).WithGasMeter(stypes.NewInfiniteGasMeter()), + exp: false, + }, + "any regular block": { + ctx: sdk.Context{}.WithBlockHeader(tmproto.Header{Height: 1}).WithGasMeter(stypes.NewGasMeter(10000000)), + exp: false, + }, + "simulation": { + ctx: sdk.Context{}.WithBlockHeader(tmproto.Header{Height: 1}).WithGasMeter(stypes.NewInfiniteGasMeter()), + exp: true, + }, + } + for msg := range specs { + t.Run(msg, func(t *testing.T) { + // assert.Equal(t, spec.exp, isSimulationMode(spec.ctx)) + }) + } +} + +func TestCreateWithGzippedPayload(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm.gzip") + require.NoError(t, err, "reading gzipped WASM code") + + contractID, _, err := keeper.Create(ctx, creator, wasmCode, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + // and verify content + storedCode, err := keepers.WasmKeeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, hackatomWasm, storedCode) +} + +func TestCreateWithBrokenGzippedPayload(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + wasmCode, err := os.ReadFile("./testdata/broken_crc.gzip") + require.NoError(t, err, "reading gzipped WASM code") + + gm := sdk.NewInfiniteGasMeter() + codeID, checksum, err := keeper.Create(ctx.WithGasMeter(gm), creator, wasmCode, nil) + require.Error(t, err) + assert.Empty(t, codeID) + assert.Empty(t, checksum) + assert.GreaterOrEqual(t, gm.GasConsumed(), sdk.Gas(121384)) // 809232 * 0.15 (default uncompress costs) = 121384 +} + +func TestInstantiate(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := sdk.AccAddress(bytes.Repeat([]byte{1}, address.Len)) + keepers.Faucet.Fund(ctx, creator, deposit...) + example := StoreHackatomExampleContract(t, ctx, keepers) + + initMsg := HackatomExampleInitMsg{ + Verifier: RandomAccountAddress(t), + Beneficiary: RandomAccountAddress(t), + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + gasBefore := ctx.GasMeter().GasConsumed() + + em := sdk.NewEventManager() + // create with no balance is also legal + gotContractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil) + require.NoError(t, err) + require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", gotContractAddr.String()) + + gasAfter := ctx.GasMeter().GasConsumed() + if types.EnableGasVerification { + require.Equal(t, uint64(0x1a7b6), gasAfter-gasBefore) + } + + // ensure it is stored properly + info := keepers.WasmKeeper.GetContractInfo(ctx, gotContractAddr) + require.NotNil(t, info) + assert.Equal(t, creator.String(), info.Creator) + assert.Equal(t, example.CodeID, info.CodeID) + assert.Equal(t, "demo contract 1", info.Label) + + exp := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: example.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: initMsgBz, + }} + assert.Equal(t, exp, keepers.WasmKeeper.GetContractHistory(ctx, gotContractAddr)) + + // and events emitted + expEvt := sdk.Events{ + sdk.NewEvent("instantiate", + sdk.NewAttribute("_contract_address", gotContractAddr.String()), sdk.NewAttribute("code_id", "1")), + sdk.NewEvent("wasm", + sdk.NewAttribute("_contract_address", gotContractAddr.String()), sdk.NewAttribute("Let the", "hacking begin")), + } + assert.Equal(t, expEvt, em.Events()) +} + +func TestInstantiateWithDeposit(t *testing.T) { + var ( + bob = bytes.Repeat([]byte{1}, types.SDKAddrLen) + fred = bytes.Repeat([]byte{2}, types.SDKAddrLen) + + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + initMsg = mustMarshal(t, HackatomExampleInitMsg{Verifier: fred, Beneficiary: bob}) + ) + + specs := map[string]struct { + srcActor sdk.AccAddress + expError bool + fundAddr bool + }{ + "address with funds": { + srcActor: bob, + fundAddr: true, + }, + "address without funds": { + srcActor: bob, + expError: true, + }, + "blocked address": { + srcActor: authtypes.NewModuleAddress(authtypes.FeeCollectorName), + fundAddr: true, + expError: false, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, bankKeeper, keeper := keepers.AccountKeeper, keepers.BankKeeper, keepers.ContractKeeper + + if spec.fundAddr { + fundAccounts(t, ctx, accKeeper, bankKeeper, spec.srcActor, sdk.NewCoins(sdk.NewInt64Coin("denom", 200))) + } + contractID, _, err := keeper.Create(ctx, spec.srcActor, hackatomWasm, nil) + require.NoError(t, err) + + // when + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, spec.srcActor, nil, initMsg, "my label", deposit) + // then + if spec.expError { + require.Error(t, err) + return + } + require.NoError(t, err) + balances := bankKeeper.GetAllBalances(ctx, addr) + assert.Equal(t, deposit, balances) + }) + } +} + +func TestInstantiateWithPermissions(t *testing.T) { + var ( + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + myAddr = bytes.Repeat([]byte{1}, types.SDKAddrLen) + otherAddr = bytes.Repeat([]byte{2}, types.SDKAddrLen) + anyAddr = bytes.Repeat([]byte{3}, types.SDKAddrLen) + ) + + initMsg := HackatomExampleInitMsg{ + Verifier: anyAddr, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + specs := map[string]struct { + srcPermission types.AccessConfig + srcActor sdk.AccAddress + expError *sdkerrors.Error + }{ + "default": { + srcPermission: types.DefaultUploadAccess, + srcActor: anyAddr, + }, + "everybody": { + srcPermission: types.AllowEverybody, + srcActor: anyAddr, + }, + "nobody": { + srcPermission: types.AllowNobody, + srcActor: myAddr, + expError: sdkerrors.ErrUnauthorized, + }, + "onlyAddress with matching address": { + srcPermission: types.AccessTypeOnlyAddress.With(myAddr), + srcActor: myAddr, + }, + "onlyAddress with non matching address": { + srcActor: myAddr, + srcPermission: types.AccessTypeOnlyAddress.With(otherAddr), + expError: sdkerrors.ErrUnauthorized, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, bankKeeper, keeper := keepers.AccountKeeper, keepers.BankKeeper, keepers.ContractKeeper + fundAccounts(t, ctx, accKeeper, bankKeeper, spec.srcActor, deposit) + + contractID, _, err := keeper.Create(ctx, myAddr, hackatomWasm, &spec.srcPermission) + require.NoError(t, err) + + _, _, err = keepers.ContractKeeper.Instantiate(ctx, contractID, spec.srcActor, nil, initMsgBz, "demo contract 1", nil) + assert.True(t, spec.expError.Is(err), "got %+v", err) + }) + } +} + +func TestInstantiateWithAccounts(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + example := StoreHackatomExampleContract(t, parentCtx, keepers) + require.Equal(t, uint64(1), example.CodeID) + initMsg := mustMarshal(t, HackatomExampleInitMsg{Verifier: RandomAccountAddress(t), Beneficiary: RandomAccountAddress(t)}) + + senderAddr := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(parentCtx, senderAddr, sdk.NewInt64Coin("denom", 100000000)) + const myLabel = "testing" + mySalt := []byte(`my salt`) + contractAddr := BuildContractAddressPredictable(example.Checksum, senderAddr, mySalt, []byte{}) + + lastAccountNumber := keepers.AccountKeeper.GetAccount(parentCtx, senderAddr).GetAccountNumber() + + specs := map[string]struct { + option Option + account authtypes.AccountI + initBalance sdk.Coin + deposit sdk.Coins + expErr error + expAccount authtypes.AccountI + expBalance sdk.Coins + }{ + "unused BaseAccount exists": { + account: authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + initBalance: sdk.NewInt64Coin("denom", 100000000), + expAccount: authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+1, 0), // +1 for next seq + expBalance: sdk.NewCoins(sdk.NewInt64Coin("denom", 100000000)), + }, + "BaseAccount with sequence exists": { + account: authtypes.NewBaseAccount(contractAddr, nil, 0, 1), + expErr: types.ErrAccountExists, + }, + "BaseAccount with pubkey exists": { + account: authtypes.NewBaseAccount(contractAddr, &ed25519.PubKey{}, 0, 0), + expErr: types.ErrAccountExists, + }, + "no account existed": { + expAccount: authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+1, 0), // +1 for next seq, + expBalance: sdk.NewCoins(), + }, + "no account existed before create with deposit": { + expAccount: authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+1, 0), // +1 for next seq + deposit: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), + expBalance: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), + }, + "prunable DelayedVestingAccount gets overwritten": { + account: vestingtypes.NewDelayedVestingAccount( + authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), time.Now().Add(30*time.Hour).Unix()), + initBalance: sdk.NewCoin("denom", sdk.NewInt(1_000)), + deposit: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1))), + expAccount: authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+2, 0), // +1 for next seq, +1 for spec.account created + expBalance: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1))), + }, + "prunable ContinuousVestingAccount gets overwritten": { + account: vestingtypes.NewContinuousVestingAccount( + authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), time.Now().Add(time.Hour).Unix(), time.Now().Add(2*time.Hour).Unix()), + initBalance: sdk.NewCoin("denom", sdk.NewInt(1_000)), + deposit: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1))), + expAccount: authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+2, 0), // +1 for next seq, +1 for spec.account created + expBalance: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1))), + }, + "prunable account without balance gets overwritten": { + account: vestingtypes.NewContinuousVestingAccount( + authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(0))), time.Now().Add(time.Hour).Unix(), time.Now().Add(2*time.Hour).Unix()), + expAccount: authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+2, 0), // +1 for next seq, +1 for spec.account created + expBalance: sdk.NewCoins(), + }, + "unknown account type is rejected with error": { + account: authtypes.NewModuleAccount( + authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + "testing", + ), + initBalance: sdk.NewCoin("denom", sdk.NewInt(1_000)), + expErr: types.ErrAccountExists, + }, + "with option used to set non default type to accept list": { + option: WithAcceptedAccountTypesOnContractInstantiation(&vestingtypes.DelayedVestingAccount{}), + account: vestingtypes.NewDelayedVestingAccount( + authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), time.Now().Add(30*time.Hour).Unix()), + initBalance: sdk.NewCoin("denom", sdk.NewInt(1_000)), + deposit: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1))), + expAccount: vestingtypes.NewDelayedVestingAccount(authtypes.NewBaseAccount(contractAddr, nil, lastAccountNumber+1, 0), + sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), time.Now().Add(30*time.Hour).Unix()), + expBalance: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_001))), + }, + "pruning account fails": { + option: WithAccountPruner(wasmtesting.AccountPrunerMock{CleanupExistingAccountFn: func(ctx sdk.Context, existingAccount authtypes.AccountI) (handled bool, err error) { + return false, types.ErrUnsupportedForContract.Wrap("testing") + }}), + account: vestingtypes.NewDelayedVestingAccount( + authtypes.NewBaseAccount(contractAddr, nil, 0, 0), + sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))), time.Now().Add(30*time.Hour).Unix()), + expErr: types.ErrUnsupportedForContract, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + if spec.account != nil { + keepers.AccountKeeper.SetAccount(ctx, keepers.AccountKeeper.NewAccount(ctx, spec.account)) + } + if !spec.initBalance.IsNil() { + keepers.Faucet.Fund(ctx, spec.account.GetAddress(), spec.initBalance) + } + if spec.option != nil { + spec.option.apply(keepers.WasmKeeper) + } + defer func() { + if spec.option != nil { // reset + WithAcceptedAccountTypesOnContractInstantiation(&authtypes.BaseAccount{}).apply(keepers.WasmKeeper) + WithAccountPruner(NewVestingCoinBurner(keepers.BankKeeper)).apply(keepers.WasmKeeper) + } + }() + // when + gotAddr, _, gotErr := keepers.ContractKeeper.Instantiate2(ctx, 1, senderAddr, nil, initMsg, myLabel, spec.deposit, mySalt, false) + if spec.expErr != nil { + assert.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, contractAddr, gotAddr) + // and + gotAcc := keepers.AccountKeeper.GetAccount(ctx, contractAddr) + assert.Equal(t, spec.expAccount, gotAcc) + // and + gotBalance := keepers.BankKeeper.GetAllBalances(ctx, contractAddr) + assert.Equal(t, spec.expBalance, gotBalance) + }) + } +} + +func TestInstantiateWithNonExistingCodeID(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + initMsg := HackatomExampleInitMsg{} + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + const nonExistingCodeID = 9999 + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, nonExistingCodeID, creator, nil, initMsgBz, "demo contract 2", nil) + require.True(t, types.ErrNotFound.Is(err), err) + require.Nil(t, addr) +} + +func TestInstantiateWithContractDataResponse(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + + wasmerMock := &wasmtesting.MockWasmer{ + InstantiateFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{Data: []byte("my-response-data")}, 0, nil + }, + AnalyzeCodeFn: wasmtesting.WithoutIBCAnalyzeFn, + CreateFn: wasmtesting.NoOpCreateFn, + } + + example := StoreRandomContract(t, ctx, keepers, wasmerMock) + _, data, err := keepers.ContractKeeper.Instantiate(ctx, example.CodeID, example.CreatorAddr, nil, nil, "test", nil) + require.NoError(t, err) + assert.Equal(t, []byte("my-response-data"), data) +} + +func TestInstantiateWithContractFactoryChildQueriesParent(t *testing.T) { + // Scenario: + // given a factory contract stored + // when instantiated, the contract creates a new child contract instance + // and the child contracts queries the senders ContractInfo on instantiation + // then the factory contract's ContractInfo should be returned to the child contract + // + // see also: https://github.com/CosmWasm/wasmd/issues/896 + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + var instantiationCount int + callbacks := make([]func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error), 2) + wasmerMock := &wasmtesting.MockWasmer{ + // dispatch instantiation calls to callbacks + InstantiateFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + require.Greater(t, len(callbacks), instantiationCount, "unexpected call to instantiation") + do := callbacks[instantiationCount] + instantiationCount++ + return do(codeID, env, info, initMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) + }, + AnalyzeCodeFn: wasmtesting.WithoutIBCAnalyzeFn, + CreateFn: wasmtesting.NoOpCreateFn, + } + + // overwrite wasmvm in router + router := baseapp.NewMsgServiceRouter() + router.SetInterfaceRegistry(keepers.EncodingConfig.InterfaceRegistry) + types.RegisterMsgServer(router, NewMsgServerImpl(NewDefaultPermissionKeeper(keeper))) + keeper.messenger = NewDefaultMessageHandler(router, nil, nil, nil, keepers.EncodingConfig.Marshaler, nil) + // overwrite wasmvm in response handler + keeper.wasmVMResponseHandler = NewDefaultWasmVMContractResponseHandler(NewMessageDispatcher(keeper.messenger, keeper)) + + example := StoreRandomContract(t, ctx, keepers, wasmerMock) + // factory contract + callbacks[0] = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + t.Log("called factory") + return &wasmvmtypes.Response{Data: []byte("parent"), Messages: []wasmvmtypes.SubMsg{ + { + ID: 1, ReplyOn: wasmvmtypes.ReplyNever, + Msg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Instantiate: &wasmvmtypes.InstantiateMsg{CodeID: example.CodeID, Msg: []byte(`{}`), Label: "child"}, + }, + }, + }, + }}, 0, nil + } + + // child contract + var capturedSenderAddr string + var capturedCodeInfo []byte + callbacks[1] = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + t.Log("called child") + capturedSenderAddr = info.Sender + var err error + capturedCodeInfo, err = querier.Query(wasmvmtypes.QueryRequest{ + Wasm: &wasmvmtypes.WasmQuery{ + ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: info.Sender}, + }, + }, gasLimit) + require.NoError(t, err) + return &wasmvmtypes.Response{Data: []byte("child")}, 0, nil + } + + // when + parentAddr, data, err := keepers.ContractKeeper.Instantiate(ctx, example.CodeID, example.CreatorAddr, nil, nil, "test", nil) + + // then + require.NoError(t, err) + assert.Equal(t, []byte("parent"), data) + require.Equal(t, parentAddr.String(), capturedSenderAddr) + expCodeInfo := fmt.Sprintf(`{"code_id":%d,"creator":%q,"pinned":false}`, example.CodeID, example.CreatorAddr.String()) + assert.JSONEq(t, expCodeInfo, string(capturedCodeInfo)) +} + +func TestExecute(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + bob := RandomAccountAddress(t) + + contractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 3", deposit) + require.NoError(t, err) + require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", 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, bankKeeper.GetAllBalances(ctx, creatorAcct.GetAddress())) + + // ensure contract has updated balance + contractAcct := accKeeper.GetAccount(ctx, addr) + require.NotNil(t, contractAcct) + assert.Equal(t, deposit, bankKeeper.GetAllBalances(ctx, contractAcct.GetAddress())) + + // unauthorized - trialCtx so we don't change state + trialCtx := ctx.WithMultiStore(ctx.MultiStore().CacheWrap().(sdk.MultiStore)) + res, err := keepers.ContractKeeper.Execute(trialCtx, addr, creator, []byte(`{"release":{}}`), nil) + require.Error(t, err) + require.True(t, errors.Is(err, types.ErrExecuteFailed)) + require.Equal(t, "Unauthorized: execute wasm contract failed", err.Error()) + + // verifier can execute, and get proper gas amount + start := time.Now() + gasBefore := ctx.GasMeter().GasConsumed() + em := sdk.NewEventManager() + // when + res, err = keepers.ContractKeeper.Execute(ctx.WithEventManager(em), addr, fred, []byte(`{"release":{}}`), topUp) + diff := time.Now().Sub(start) + require.NoError(t, err) + require.NotNil(t, res) + + // make sure gas is properly deducted from ctx + gasAfter := ctx.GasMeter().GasConsumed() + if types.EnableGasVerification { + require.Equal(t, uint64(0x17d7f), gasAfter-gasBefore) + } + // ensure bob now exists and got both payments released + bobAcct = accKeeper.GetAccount(ctx, bob) + require.NotNil(t, bobAcct) + balance := bankKeeper.GetAllBalances(ctx, bobAcct.GetAddress()) + 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{}, bankKeeper.GetAllBalances(ctx, contractAcct.GetAddress())) + + // and events emitted + require.Len(t, em.Events(), 9) + expEvt := sdk.NewEvent("execute", + sdk.NewAttribute("_contract_address", addr.String())) + assert.Equal(t, expEvt, em.Events()[3], prettyEvents(t, em.Events())) + + t.Logf("Duration: %v (%d gas)\n", diff, gasAfter-gasBefore) +} + +func TestExecuteWithDeposit(t *testing.T) { + var ( + bob = bytes.Repeat([]byte{1}, types.SDKAddrLen) + fred = bytes.Repeat([]byte{2}, types.SDKAddrLen) + blockedAddr = authtypes.NewModuleAddress(distributiontypes.ModuleName) + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + ) + + specs := map[string]struct { + srcActor sdk.AccAddress + beneficiary sdk.AccAddress + newBankParams *banktypes.Params + expError bool + fundAddr bool + }{ + "actor with funds": { + srcActor: bob, + fundAddr: true, + beneficiary: fred, + }, + "actor without funds": { + srcActor: bob, + beneficiary: fred, + expError: true, + }, + "blocked address as actor": { + srcActor: blockedAddr, + fundAddr: true, + beneficiary: fred, + expError: false, + }, + "coin transfer with all transfers disabled": { + srcActor: bob, + fundAddr: true, + beneficiary: fred, + newBankParams: &banktypes.Params{DefaultSendEnabled: false}, + expError: true, + }, + "coin transfer with transfer denom disabled": { + srcActor: bob, + fundAddr: true, + beneficiary: fred, + newBankParams: &banktypes.Params{ + DefaultSendEnabled: true, + SendEnabled: []*banktypes.SendEnabled{{Denom: "denom", Enabled: false}}, + }, + expError: true, + }, + "blocked address as beneficiary": { + srcActor: bob, + fundAddr: true, + beneficiary: blockedAddr, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, bankKeeper, keeper := keepers.AccountKeeper, keepers.BankKeeper, keepers.ContractKeeper + if spec.newBankParams != nil { + bankKeeper.SetParams(ctx, *spec.newBankParams) + } + if spec.fundAddr { + fundAccounts(t, ctx, accKeeper, bankKeeper, spec.srcActor, sdk.NewCoins(sdk.NewInt64Coin("denom", 200))) + } + codeID, _, err := keeper.Create(ctx, spec.srcActor, hackatomWasm, nil) + require.NoError(t, err) + + initMsg := HackatomExampleInitMsg{Verifier: spec.srcActor, Beneficiary: spec.beneficiary} + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, spec.srcActor, nil, initMsgBz, "my label", nil) + require.NoError(t, err) + + // when + _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, spec.srcActor, []byte(`{"release":{}}`), deposit) + + // then + if spec.expError { + require.Error(t, err) + return + } + require.NoError(t, err) + balances := bankKeeper.GetAllBalances(ctx, spec.beneficiary) + assert.Equal(t, deposit, balances) + }) + } +} + +func TestExecuteWithNonExistingAddress(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + + // unauthorized - trialCtx so we don't change state + nonExistingAddress := RandomAccountAddress(t) + _, err := keeper.Execute(ctx, nonExistingAddress, creator, []byte(`{}`), nil) + require.True(t, types.ErrNotFound.Is(err), err) +} + +func TestExecuteWithPanic(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + + contractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 4", deposit) + require.NoError(t, err) + + // let's make sure we get a reasonable error, no panic/crash + _, err = keepers.ContractKeeper.Execute(ctx, addr, fred, []byte(`{"panic":{}}`), topUp) + require.Error(t, err) + require.True(t, errors.Is(err, types.ErrExecuteFailed)) + // test with contains as "Display" implementation of the Wasmer "RuntimeError" is different for Mac and Linux + assert.Contains(t, err.Error(), "Error calling the VM: Error executing Wasm: Wasmer runtime error: RuntimeError: Aborted: panicked at 'This page intentionally faulted', src/contract.rs:169:5: execute wasm contract failed") +} + +func TestExecuteWithCpuLoop(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + + contractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 5", deposit) + require.NoError(t, err) + + // make sure we set a limit before calling + var gasLimit uint64 = 400_000 + ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // ensure we get an out of gas panic + defer func() { + r := recover() + require.NotNil(t, r) + _, ok := r.(sdk.ErrorOutOfGas) + require.True(t, ok, "%v", r) + }() + + // this should throw out of gas exception (panic) + _, err = keepers.ContractKeeper.Execute(ctx, addr, fred, []byte(`{"cpu_loop":{}}`), nil) + require.True(t, false, "We must panic before this line") +} + +func TestExecuteWithStorageLoop(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + + contractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 6", deposit) + require.NoError(t, err) + + // make sure we set a limit before calling + var gasLimit uint64 = 400_002 + ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // ensure we get an out of gas panic + defer func() { + r := recover() + require.NotNil(t, r) + _, ok := r.(sdk.ErrorOutOfGas) + require.True(t, ok, "%v", r) + }() + + // this should throw out of gas exception (panic) + _, err = keepers.ContractKeeper.Execute(ctx, addr, fred, []byte(`{"storage_loop":{}}`), nil) + require.True(t, false, "We must panic before this line") +} + +func TestMigrate(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(parentCtx, creator, deposit.Add(deposit...)...) + fred := DeterministicAccountAddress(t, 2) + keepers.Faucet.Fund(parentCtx, fred, topUp...) + + originalCodeID := StoreHackatomExampleContract(t, parentCtx, keepers).CodeID + newCodeID := StoreHackatomExampleContract(t, parentCtx, keepers).CodeID + ibcCodeID := StoreIBCReflectContract(t, parentCtx, keepers).CodeID + require.NotEqual(t, originalCodeID, newCodeID) + + restrictedCodeExample := StoreHackatomExampleContract(t, parentCtx, keepers) + require.NoError(t, keeper.SetAccessConfig(parentCtx, restrictedCodeExample.CodeID, restrictedCodeExample.CreatorAddr, types.AllowNobody)) + require.NotEqual(t, originalCodeID, restrictedCodeExample.CodeID) + + anyAddr := RandomAccountAddress(t) + newVerifierAddr := RandomAccountAddress(t) + initMsgBz := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: anyAddr, + }.GetBytes(t) + + migMsg := struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: newVerifierAddr} + migMsgBz, err := json.Marshal(migMsg) + require.NoError(t, err) + + specs := map[string]struct { + admin sdk.AccAddress + overrideContractAddr sdk.AccAddress + caller sdk.AccAddress + fromCodeID uint64 + toCodeID uint64 + migrateMsg []byte + expErr *sdkerrors.Error + expVerifier sdk.AccAddress + expIBCPort bool + initMsg []byte + }{ + "all good with same code id": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: originalCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with different code id": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: newCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with admin set": { + admin: fred, + caller: fred, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: newCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "adds IBC port for IBC enabled contracts": { + admin: fred, + caller: fred, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: ibcCodeID, + migrateMsg: []byte(`{}`), + expIBCPort: true, + expVerifier: fred, // not updated + }, + "prevent migration when admin was not set on instantiate": { + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: originalCodeID, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent migration when not sent by admin": { + caller: creator, + admin: fred, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: originalCodeID, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent migration when new code is restricted": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: restrictedCodeExample.CodeID, + migrateMsg: migMsgBz, + expErr: sdkerrors.ErrUnauthorized, + }, + "fail with non existing code id": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: 99999, + expErr: sdkerrors.ErrInvalidRequest, + }, + "fail with non existing contract addr": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + overrideContractAddr: anyAddr, + fromCodeID: originalCodeID, + toCodeID: originalCodeID, + expErr: sdkerrors.ErrInvalidRequest, + }, + "fail in contract with invalid migrate msg": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: originalCodeID, + migrateMsg: bytes.Repeat([]byte{0x1}, 7), + expErr: types.ErrMigrationFailed, + }, + "fail in contract without migrate msg": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: originalCodeID, + expErr: types.ErrMigrationFailed, + }, + "fail when no IBC callbacks": { + admin: fred, + caller: fred, + initMsg: IBCReflectInitMsg{ReflectCodeID: StoreReflectContract(t, parentCtx, keepers).CodeID}.GetBytes(t), + fromCodeID: ibcCodeID, + toCodeID: newCodeID, + migrateMsg: migMsgBz, + expErr: types.ErrMigrationFailed, + }, + } + + blockHeight := parentCtx.BlockHeight() + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + // given a contract instance + ctx, _ := parentCtx.WithBlockHeight(blockHeight + 1).CacheContext() + blockHeight++ + + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, spec.fromCodeID, creator, spec.admin, spec.initMsg, "demo contract", nil) + require.NoError(t, err) + if spec.overrideContractAddr != nil { + contractAddr = spec.overrideContractAddr + } + // when + _, err = keeper.Migrate(ctx, contractAddr, spec.caller, spec.toCodeID, spec.migrateMsg) + + // then + require.True(t, spec.expErr.Is(err), "expected %v but got %+v", spec.expErr, err) + if spec.expErr != nil { + return + } + cInfo := keepers.WasmKeeper.GetContractInfo(ctx, contractAddr) + assert.Equal(t, spec.toCodeID, cInfo.CodeID) + assert.Equal(t, spec.expIBCPort, cInfo.IBCPortID != "", cInfo.IBCPortID) + + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: spec.fromCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: initMsgBz, + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: spec.toCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: spec.migrateMsg, + }} + assert.Equal(t, expHistory, keepers.WasmKeeper.GetContractHistory(ctx, contractAddr)) + + // and verify contract state + raw := keepers.WasmKeeper.QueryRaw(ctx, contractAddr, []byte("config")) + var stored map[string]string + require.NoError(t, json.Unmarshal(raw, &stored)) + require.Contains(t, stored, "verifier") + require.NoError(t, err) + assert.Equal(t, spec.expVerifier.String(), stored["verifier"]) + }) + } +} + +func TestMigrateReplacesTheSecondIndex(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + example := InstantiateHackatomExampleContract(t, ctx, keepers) + + // then assert a second index exists + store := ctx.KVStore(keepers.WasmKeeper.storeKey) + oldContractInfo := keepers.WasmKeeper.GetContractInfo(ctx, example.Contract) + require.NotNil(t, oldContractInfo) + createHistoryEntry := types.ContractCodeHistoryEntry{ + CodeID: example.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + } + exists := store.Has(types.GetContractByCreatedSecondaryIndexKey(example.Contract, createHistoryEntry)) + require.True(t, exists) + + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) // increment for different block + // when do migrate + newCodeExample := StoreBurnerExampleContract(t, ctx, keepers) + migMsgBz := BurnerExampleInitMsg{Payout: example.CreatorAddr}.GetBytes(t) + _, err := keepers.ContractKeeper.Migrate(ctx, example.Contract, example.CreatorAddr, newCodeExample.CodeID, migMsgBz) + require.NoError(t, err) + + // then the new index exists + migrateHistoryEntry := types.ContractCodeHistoryEntry{ + CodeID: newCodeExample.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + } + exists = store.Has(types.GetContractByCreatedSecondaryIndexKey(example.Contract, migrateHistoryEntry)) + require.True(t, exists) + // and the old index was removed + exists = store.Has(types.GetContractByCreatedSecondaryIndexKey(example.Contract, createHistoryEntry)) + require.False(t, exists) +} + +func TestMigrateWithDispatchedMessage(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(ctx, sdk.NewInt64Coin("denom", 5000)) + + burnerCode, err := os.ReadFile("./testdata/burner.wasm") + require.NoError(t, err) + + originalContractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + burnerContractID, _, err := keeper.Create(ctx, creator, burnerCode, nil) + require.NoError(t, err) + require.NotEqual(t, originalContractID, burnerContractID) + + _, _, myPayoutAddr := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: fred, + } + initMsgBz := initMsg.GetBytes(t) + + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, originalContractID, creator, fred, initMsgBz, "demo contract", deposit) + require.NoError(t, err) + + migMsgBz := BurnerExampleInitMsg{Payout: myPayoutAddr}.GetBytes(t) + ctx = ctx.WithEventManager(sdk.NewEventManager()).WithBlockHeight(ctx.BlockHeight() + 1) + data, err := keeper.Migrate(ctx, contractAddr, fred, burnerContractID, migMsgBz) + require.NoError(t, err) + assert.Equal(t, "burnt 1 keys", string(data)) + type dict map[string]interface{} + expEvents := []dict{ + { + "Type": "migrate", + "Attr": []dict{ + {"code_id": "2"}, + {"_contract_address": contractAddr}, + }, + }, + { + "Type": "wasm", + "Attr": []dict{ + {"_contract_address": contractAddr}, + {"action": "burn"}, + {"payout": myPayoutAddr}, + }, + }, + { + "Type": "coin_spent", + "Attr": []dict{ + {"spender": contractAddr}, + {"amount": "100000denom"}, + }, + }, + { + "Type": "coin_received", + "Attr": []dict{ + {"receiver": myPayoutAddr}, + {"amount": "100000denom"}, + }, + }, + { + "Type": "transfer", + "Attr": []dict{ + {"recipient": myPayoutAddr}, + {"sender": contractAddr}, + {"amount": "100000denom"}, + }, + }, + } + expJSONEvts := string(mustMarshal(t, expEvents)) + assert.JSONEq(t, expJSONEvts, prettyEvents(t, ctx.EventManager().Events()), prettyEvents(t, ctx.EventManager().Events())) + + // all persistent data cleared + m := keepers.WasmKeeper.QueryRaw(ctx, contractAddr, []byte("config")) + require.Len(t, m, 0) + + // and all deposit tokens sent to myPayoutAddr + balance := keepers.BankKeeper.GetAllBalances(ctx, myPayoutAddr) + assert.Equal(t, deposit, balance) +} + +func TestIterateContractsByCode(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k, c := keepers.WasmKeeper, keepers.ContractKeeper + example1 := InstantiateHackatomExampleContract(t, ctx, keepers) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + example2 := InstantiateIBCReflectContract(t, ctx, keepers) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + initMsg := HackatomExampleInitMsg{ + Verifier: RandomAccountAddress(t), + Beneficiary: RandomAccountAddress(t), + }.GetBytes(t) + contractAddr3, _, err := c.Instantiate(ctx, example1.CodeID, example1.CreatorAddr, nil, initMsg, "foo", nil) + require.NoError(t, err) + specs := map[string]struct { + codeID uint64 + exp []sdk.AccAddress + }{ + "multiple results": { + codeID: example1.CodeID, + exp: []sdk.AccAddress{example1.Contract, contractAddr3}, + }, + "single results": { + codeID: example2.CodeID, + exp: []sdk.AccAddress{example2.Contract}, + }, + "empty results": { + codeID: 99999, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var gotAddr []sdk.AccAddress + k.IterateContractsByCode(ctx, spec.codeID, func(address sdk.AccAddress) bool { + gotAddr = append(gotAddr, address) + return false + }) + assert.Equal(t, spec.exp, gotAddr) + }) + } +} + +func TestIterateContractsByCodeWithMigration(t *testing.T) { + // mock migration so that it does not fail when migrate example1 to example2.codeID + mockWasmVM := wasmtesting.MockWasmer{MigrateFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, migrateMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{}, 1, nil + }} + wasmtesting.MakeInstantiable(&mockWasmVM) + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithWasmEngine(&mockWasmVM)) + k, c := keepers.WasmKeeper, keepers.ContractKeeper + example1 := InstantiateHackatomExampleContract(t, ctx, keepers) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + example2 := InstantiateIBCReflectContract(t, ctx, keepers) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + _, err := c.Migrate(ctx, example1.Contract, example1.CreatorAddr, example2.CodeID, []byte("{}")) + require.NoError(t, err) + + // when + var gotAddr []sdk.AccAddress + k.IterateContractsByCode(ctx, example2.CodeID, func(address sdk.AccAddress) bool { + gotAddr = append(gotAddr, address) + return false + }) + + // then + exp := []sdk.AccAddress{example2.Contract, example1.Contract} + assert.Equal(t, exp, gotAddr) +} + +type sudoMsg struct { + // This is a tongue-in-check demo command. This is not the intended purpose of Sudo. + // Here we show that some priviledged Go module can make a call that should never be exposed + // to end users (via Tx/Execute). + // + // The contract developer can choose to expose anything to sudo. This functionality is not a true + // backdoor (it can never be called by end users), but allows the developers of the native blockchain + // code to make special calls. This can also be used as an authentication mechanism, if you want to expose + // some callback that only can be triggered by some system module and not faked by external users. + StealFunds stealFundsMsg `json:"steal_funds"` +} + +type stealFundsMsg struct { + Recipient string `json:"recipient"` + Amount wasmvmtypes.Coins `json:"amount"` +} + +func TestSudo(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + + contractID, _, err := keeper.Create(ctx, creator, hackatomWasm, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 3", deposit) + require.NoError(t, err) + require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", addr.String()) + + // the community is broke + _, _, community := keyPubAddr() + comAcct := accKeeper.GetAccount(ctx, community) + require.Nil(t, comAcct) + + // now the community wants to get paid via sudo + msg := sudoMsg{ + // This is a tongue-in-check demo command. This is not the intended purpose of Sudo. + // Here we show that some priviledged Go module can make a call that should never be exposed + // to end users (via Tx/Execute). + StealFunds: stealFundsMsg{ + Recipient: community.String(), + Amount: wasmvmtypes.Coins{wasmvmtypes.NewCoin(76543, "denom")}, + }, + } + sudoMsg, err := json.Marshal(msg) + require.NoError(t, err) + + em := sdk.NewEventManager() + + // when + _, err = keepers.WasmKeeper.Sudo(ctx.WithEventManager(em), addr, sudoMsg) + require.NoError(t, err) + + // ensure community now exists and got paid + comAcct = accKeeper.GetAccount(ctx, community) + require.NotNil(t, comAcct) + balance := bankKeeper.GetBalance(ctx, comAcct.GetAddress(), "denom") + assert.Equal(t, sdk.NewInt64Coin("denom", 76543), balance) + // and events emitted + require.Len(t, em.Events(), 4, prettyEvents(t, em.Events())) + expEvt := sdk.NewEvent("sudo", + sdk.NewAttribute("_contract_address", addr.String())) + assert.Equal(t, expEvt, em.Events()[0]) +} + +func prettyEvents(t *testing.T, events sdk.Events) string { + t.Helper() + type prettyEvent struct { + Type string + Attr []map[string]string + } + + r := make([]prettyEvent, len(events)) + for i, e := range events { + attr := make([]map[string]string, len(e.Attributes)) + for j, a := range e.Attributes { + attr[j] = map[string]string{string(a.Key): string(a.Value)} + } + r[i] = prettyEvent{Type: e.Type, Attr: attr} + } + return string(mustMarshal(t, r)) +} + +func mustMarshal(t *testing.T, r interface{}) []byte { + t.Helper() + bz, err := json.Marshal(r) + require.NoError(t, err) + return bz +} + +func TestUpdateContractAdmin(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(parentCtx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(parentCtx, topUp...) + + originalContractID, _, err := keeper.Create(parentCtx, creator, hackatomWasm, nil) + require.NoError(t, err) + + _, _, anyAddr := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + specs := map[string]struct { + instAdmin sdk.AccAddress + newAdmin sdk.AccAddress + overrideContractAddr sdk.AccAddress + caller sdk.AccAddress + expErr *sdkerrors.Error + }{ + "all good with admin set": { + instAdmin: fred, + newAdmin: anyAddr, + caller: fred, + }, + "prevent update when admin was not set on instantiate": { + caller: creator, + newAdmin: fred, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent updates from non admin address": { + instAdmin: creator, + newAdmin: fred, + caller: fred, + expErr: sdkerrors.ErrUnauthorized, + }, + "fail with non existing contract addr": { + instAdmin: creator, + newAdmin: anyAddr, + caller: creator, + overrideContractAddr: anyAddr, + expErr: sdkerrors.ErrInvalidRequest, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, originalContractID, creator, spec.instAdmin, initMsgBz, "demo contract", nil) + require.NoError(t, err) + if spec.overrideContractAddr != nil { + addr = spec.overrideContractAddr + } + err = keeper.UpdateContractAdmin(ctx, addr, spec.caller, spec.newAdmin) + require.True(t, spec.expErr.Is(err), "expected %v but got %+v", spec.expErr, err) + if spec.expErr != nil { + return + } + cInfo := keepers.WasmKeeper.GetContractInfo(ctx, addr) + assert.Equal(t, spec.newAdmin.String(), cInfo.Admin) + }) + } +} + +func TestClearContractAdmin(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(parentCtx, creator, deposit.Add(deposit...)...) + fred := keepers.Faucet.NewFundedRandomAccount(parentCtx, topUp...) + + originalContractID, _, err := keeper.Create(parentCtx, creator, hackatomWasm, nil) + require.NoError(t, err) + + _, _, anyAddr := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + specs := map[string]struct { + instAdmin sdk.AccAddress + overrideContractAddr sdk.AccAddress + caller sdk.AccAddress + expErr *sdkerrors.Error + }{ + "all good when called by proper admin": { + instAdmin: fred, + caller: fred, + }, + "prevent update when admin was not set on instantiate": { + caller: creator, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent updates from non admin address": { + instAdmin: creator, + caller: fred, + expErr: sdkerrors.ErrUnauthorized, + }, + "fail with non existing contract addr": { + instAdmin: creator, + caller: creator, + overrideContractAddr: anyAddr, + expErr: sdkerrors.ErrInvalidRequest, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, originalContractID, creator, spec.instAdmin, initMsgBz, "demo contract", nil) + require.NoError(t, err) + if spec.overrideContractAddr != nil { + addr = spec.overrideContractAddr + } + err = keeper.ClearContractAdmin(ctx, addr, spec.caller) + require.True(t, spec.expErr.Is(err), "expected %v but got %+v", spec.expErr, err) + if spec.expErr != nil { + return + } + cInfo := keepers.WasmKeeper.GetContractInfo(ctx, addr) + assert.Empty(t, cInfo.Admin) + }) + } +} + +func TestPinCode(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + + var capturedChecksums []wasmvm.Checksum + mock := wasmtesting.MockWasmer{PinFn: func(checksum wasmvm.Checksum) error { + capturedChecksums = append(capturedChecksums, checksum) + return nil + }} + wasmtesting.MakeInstantiable(&mock) + myCodeID := StoreRandomContract(t, ctx, keepers, &mock).CodeID + require.Equal(t, uint64(1), myCodeID) + em := sdk.NewEventManager() + + // when + gotErr := k.pinCode(ctx.WithEventManager(em), myCodeID) + + // then + require.NoError(t, gotErr) + assert.NotEmpty(t, capturedChecksums) + assert.True(t, k.IsPinnedCode(ctx, myCodeID)) + + // and events + exp := sdk.Events{sdk.NewEvent("pin_code", sdk.NewAttribute("code_id", "1"))} + assert.Equal(t, exp, em.Events()) +} + +func TestUnpinCode(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + + var capturedChecksums []wasmvm.Checksum + mock := wasmtesting.MockWasmer{ + PinFn: func(checksum wasmvm.Checksum) error { + return nil + }, + UnpinFn: func(checksum wasmvm.Checksum) error { + capturedChecksums = append(capturedChecksums, checksum) + return nil + }, + } + wasmtesting.MakeInstantiable(&mock) + myCodeID := StoreRandomContract(t, ctx, keepers, &mock).CodeID + require.Equal(t, uint64(1), myCodeID) + err := k.pinCode(ctx, myCodeID) + require.NoError(t, err) + em := sdk.NewEventManager() + + // when + gotErr := k.unpinCode(ctx.WithEventManager(em), myCodeID) + + // then + require.NoError(t, gotErr) + assert.NotEmpty(t, capturedChecksums) + assert.False(t, k.IsPinnedCode(ctx, myCodeID)) + + // and events + exp := sdk.Events{sdk.NewEvent("unpin_code", sdk.NewAttribute("code_id", "1"))} + assert.Equal(t, exp, em.Events()) +} + +func TestInitializePinnedCodes(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + + var capturedChecksums []wasmvm.Checksum + mock := wasmtesting.MockWasmer{PinFn: func(checksum wasmvm.Checksum) error { + capturedChecksums = append(capturedChecksums, checksum) + return nil + }} + wasmtesting.MakeInstantiable(&mock) + + const testItems = 3 + myCodeIDs := make([]uint64, testItems) + for i := 0; i < testItems; i++ { + myCodeIDs[i] = StoreRandomContract(t, ctx, keepers, &mock).CodeID + require.NoError(t, k.pinCode(ctx, myCodeIDs[i])) + } + capturedChecksums = nil + + // when + gotErr := k.InitializePinnedCodes(ctx) + + // then + require.NoError(t, gotErr) + require.Len(t, capturedChecksums, testItems) + for i, c := range myCodeIDs { + var exp wasmvm.Checksum = k.GetCodeInfo(ctx, c).CodeHash + assert.Equal(t, exp, capturedChecksums[i]) + } +} + +func TestPinnedContractLoops(t *testing.T) { + var capturedChecksums []wasmvm.Checksum + mock := wasmtesting.MockWasmer{PinFn: func(checksum wasmvm.Checksum) error { + capturedChecksums = append(capturedChecksums, checksum) + return nil + }} + wasmtesting.MakeInstantiable(&mock) + + // a pinned contract that calls itself via submessages should terminate with an + // error at some point + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithWasmEngine(&mock)) + k := keepers.WasmKeeper + + example := SeedNewContractInstance(t, ctx, keepers, &mock) + require.NoError(t, k.pinCode(ctx, example.CodeID)) + var loops int + anyMsg := []byte(`{}`) + mock.ExecuteFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + loops++ + return &wasmvmtypes.Response{ + Messages: []wasmvmtypes.SubMsg{ + { + ID: 1, + ReplyOn: wasmvmtypes.ReplyNever, + Msg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Execute: &wasmvmtypes.ExecuteMsg{ + ContractAddr: example.Contract.String(), + Msg: anyMsg, + }, + }, + }, + }, + }, + }, 0, nil + } + ctx = ctx.WithGasMeter(sdk.NewGasMeter(20000)) + require.PanicsWithValue(t, sdk.ErrorOutOfGas{Descriptor: "ReadFlat"}, func() { + _, err := k.execute(ctx, example.Contract, RandomAccountAddress(t), anyMsg, nil) + require.NoError(t, err) + }) + assert.True(t, ctx.GasMeter().IsOutOfGas()) + assert.Greater(t, loops, 2) +} + +func TestNewDefaultWasmVMContractResponseHandler(t *testing.T) { + specs := map[string]struct { + srcData []byte + setup func(m *wasmtesting.MockMsgDispatcher) + expErr bool + expData []byte + expEvts sdk.Events + }{ + "submessage overwrites result when set": { + srcData: []byte("otherData"), + setup: func(m *wasmtesting.MockMsgDispatcher) { + m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + return []byte("mySubMsgData"), nil + } + }, + expErr: false, + expData: []byte("mySubMsgData"), + expEvts: sdk.Events{}, + }, + "submessage overwrites result when empty": { + srcData: []byte("otherData"), + setup: func(m *wasmtesting.MockMsgDispatcher) { + m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + return []byte(""), nil + } + }, + expErr: false, + expData: []byte(""), + expEvts: sdk.Events{}, + }, + "submessage do not overwrite result when nil": { + srcData: []byte("otherData"), + setup: func(m *wasmtesting.MockMsgDispatcher) { + m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + return nil, nil + } + }, + expErr: false, + expData: []byte("otherData"), + expEvts: sdk.Events{}, + }, + "submessage error aborts process": { + setup: func(m *wasmtesting.MockMsgDispatcher) { + m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + return nil, errors.New("test - ignore") + } + }, + expErr: true, + }, + "message emit non message events": { + setup: func(m *wasmtesting.MockMsgDispatcher) { + m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + ctx.EventManager().EmitEvent(sdk.NewEvent("myEvent")) + return nil, nil + } + }, + expEvts: sdk.Events{sdk.NewEvent("myEvent")}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var msgs []wasmvmtypes.SubMsg + var mock wasmtesting.MockMsgDispatcher + spec.setup(&mock) + d := NewDefaultWasmVMContractResponseHandler(&mock) + em := sdk.NewEventManager() + + // when + gotData, gotErr := d.Handle(sdk.Context{}.WithEventManager(em), RandomAccountAddress(t), "ibc-port", msgs, spec.srcData) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expData, gotData) + assert.Equal(t, spec.expEvts, em.Events()) + }) + } +} + +func TestReply(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + var mock wasmtesting.MockWasmer + wasmtesting.MakeInstantiable(&mock) + example := SeedNewContractInstance(t, ctx, keepers, &mock) + + specs := map[string]struct { + replyFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + expData []byte + expErr bool + expEvt sdk.Events + }{ + "all good": { + replyFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{Data: []byte("foo")}, 1, nil + }, + expData: []byte("foo"), + expEvt: sdk.Events{sdk.NewEvent("reply", sdk.NewAttribute("_contract_address", example.Contract.String()))}, + }, + "with query": { + replyFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + bzRsp, err := querier.Query(wasmvmtypes.QueryRequest{ + Bank: &wasmvmtypes.BankQuery{ + Balance: &wasmvmtypes.BalanceQuery{Address: env.Contract.Address, Denom: "stake"}, + }, + }, 10_000*DefaultGasMultiplier) + require.NoError(t, err) + var gotBankRsp wasmvmtypes.BalanceResponse + require.NoError(t, json.Unmarshal(bzRsp, &gotBankRsp)) + assert.Equal(t, wasmvmtypes.BalanceResponse{Amount: wasmvmtypes.NewCoin(0, "stake")}, gotBankRsp) + return &wasmvmtypes.Response{Data: []byte("foo")}, 1, nil + }, + expData: []byte("foo"), + expEvt: sdk.Events{sdk.NewEvent("reply", sdk.NewAttribute("_contract_address", example.Contract.String()))}, + }, + "with query error handled": { + replyFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + bzRsp, err := querier.Query(wasmvmtypes.QueryRequest{}, 0) + require.Error(t, err) + assert.Nil(t, bzRsp) + return &wasmvmtypes.Response{Data: []byte("foo")}, 1, nil + }, + expData: []byte("foo"), + expEvt: sdk.Events{sdk.NewEvent("reply", sdk.NewAttribute("_contract_address", example.Contract.String()))}, + }, + "error": { + replyFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return nil, 1, errors.New("testing") + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + mock.ReplyFn = spec.replyFn + em := sdk.NewEventManager() + gotData, gotErr := k.reply(ctx.WithEventManager(em), example.Contract, wasmvmtypes.Reply{}) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expData, gotData) + assert.Equal(t, spec.expEvt, em.Events()) + }) + } +} + +func TestQueryIsolation(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + var mock wasmtesting.MockWasmer + wasmtesting.MakeInstantiable(&mock) + example := SeedNewContractInstance(t, ctx, keepers, &mock) + WithQueryHandlerDecorator(func(other WasmVMQueryHandler) WasmVMQueryHandler { + return WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + if request.Custom == nil { + return other.HandleQuery(ctx, caller, request) + } + // here we write to DB which should not be persisted + ctx.KVStore(k.storeKey).Set([]byte(`set_in_query`), []byte(`this_is_allowed`)) + return nil, nil + }) + }).apply(k) + + // when + mock.ReplyFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + _, err := querier.Query(wasmvmtypes.QueryRequest{ + Custom: []byte(`{}`), + }, 10000*DefaultGasMultiplier) + require.NoError(t, err) + return &wasmvmtypes.Response{}, 0, nil + } + em := sdk.NewEventManager() + _, gotErr := k.reply(ctx.WithEventManager(em), example.Contract, wasmvmtypes.Reply{}) + require.NoError(t, gotErr) + assert.Nil(t, ctx.KVStore(k.storeKey).Get([]byte(`set_in_query`))) +} + +func TestSetAccessConfig(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + creatorAddr := RandomAccountAddress(t) + nonCreatorAddr := RandomAccountAddress(t) + const codeID = 1 + + specs := map[string]struct { + authz AuthorizationPolicy + chainPermission types.AccessType + newConfig types.AccessConfig + caller sdk.AccAddress + expErr bool + expEvts map[string]string + }{ + "user with new permissions == chain permissions": { + authz: DefaultAuthorizationPolicy{}, + chainPermission: types.AccessTypeEverybody, + newConfig: types.AllowEverybody, + caller: creatorAddr, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "Everybody", + }, + }, + "user with new permissions < chain permissions": { + authz: DefaultAuthorizationPolicy{}, + chainPermission: types.AccessTypeEverybody, + newConfig: types.AllowNobody, + caller: creatorAddr, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "Nobody", + }, + }, + "user with new permissions > chain permissions": { + authz: DefaultAuthorizationPolicy{}, + chainPermission: types.AccessTypeNobody, + newConfig: types.AllowEverybody, + caller: creatorAddr, + expErr: true, + }, + "different actor": { + authz: DefaultAuthorizationPolicy{}, + chainPermission: types.AccessTypeEverybody, + newConfig: types.AllowEverybody, + caller: nonCreatorAddr, + expErr: true, + }, + "gov with new permissions == chain permissions": { + authz: GovAuthorizationPolicy{}, + chainPermission: types.AccessTypeEverybody, + newConfig: types.AllowEverybody, + caller: creatorAddr, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "Everybody", + }, + }, + "gov with new permissions < chain permissions": { + authz: GovAuthorizationPolicy{}, + chainPermission: types.AccessTypeEverybody, + newConfig: types.AllowNobody, + caller: creatorAddr, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "Nobody", + }, + }, + "gov with new permissions > chain permissions": { + authz: GovAuthorizationPolicy{}, + chainPermission: types.AccessTypeNobody, + newConfig: types.AccessTypeOnlyAddress.With(creatorAddr), + caller: creatorAddr, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "OnlyAddress", + "authorized_addresses": creatorAddr.String(), + }, + }, + "gov with new permissions > chain permissions - multiple addresses": { + authz: GovAuthorizationPolicy{}, + chainPermission: types.AccessTypeNobody, + newConfig: types.AccessTypeAnyOfAddresses.With(creatorAddr, nonCreatorAddr), + caller: creatorAddr, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "AnyOfAddresses", + "authorized_addresses": creatorAddr.String() + "," + nonCreatorAddr.String(), + }, + }, + "gov without actor": { + authz: GovAuthorizationPolicy{}, + chainPermission: types.AccessTypeEverybody, + newConfig: types.AllowEverybody, + expEvts: map[string]string{ + "code_id": "1", + "code_permission": "Everybody", + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + + newParams := types.DefaultParams() + newParams.InstantiateDefaultPermission = spec.chainPermission + k.SetParams(ctx, newParams) + + k.storeCodeInfo(ctx, codeID, types.NewCodeInfo(nil, creatorAddr, types.AllowNobody)) + // when + gotErr := k.setAccessConfig(ctx, codeID, spec.caller, spec.newConfig, spec.authz) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + // and event emitted + require.Len(t, em.Events(), 1) + assert.Equal(t, "update_code_access_config", em.Events()[0].Type) + assert.Equal(t, spec.expEvts, attrsToStringMap(em.Events()[0].Attributes)) + }) + } +} + +func TestAppendToContractHistory(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + var contractAddr sdk.AccAddress = rand.Bytes(types.ContractAddrLen) + var orderedEntries []types.ContractCodeHistoryEntry + + f := fuzz.New().Funcs(ModelFuzzers...) + for i := 0; i < 10; i++ { + var entry types.ContractCodeHistoryEntry + f.Fuzz(&entry) + keepers.WasmKeeper.appendToContractHistory(ctx, contractAddr, entry) + orderedEntries = append(orderedEntries, entry) + } + // when + gotHistory := keepers.WasmKeeper.GetContractHistory(ctx, contractAddr) + assert.Equal(t, orderedEntries, gotHistory) +} + +func TestCoinBurnerPruneBalances(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + amts := sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + senderAddr := keepers.Faucet.NewFundedRandomAccount(parentCtx, amts...) + + // create vesting account + var vestingAddr sdk.AccAddress = rand.Bytes(types.ContractAddrLen) + msgCreateVestingAccount := vestingtypes.NewMsgCreateVestingAccount(senderAddr, vestingAddr, amts, time.Now().Add(time.Minute).Unix(), false) + _, err := vesting.NewMsgServerImpl(keepers.AccountKeeper, keepers.BankKeeper).CreateVestingAccount(sdk.WrapSDKContext(parentCtx), msgCreateVestingAccount) + require.NoError(t, err) + myVestingAccount := keepers.AccountKeeper.GetAccount(parentCtx, vestingAddr) + require.NotNil(t, myVestingAccount) + + specs := map[string]struct { + setupAcc func(t *testing.T, ctx sdk.Context) authtypes.AccountI + expBalances sdk.Coins + expHandled bool + expErr *sdkerrors.Error + }{ + "vesting account - all removed": { + setupAcc: func(t *testing.T, ctx sdk.Context) authtypes.AccountI { return myVestingAccount }, + expBalances: sdk.NewCoins(), + expHandled: true, + }, + "vesting account with other tokens - only original denoms removed": { + setupAcc: func(t *testing.T, ctx sdk.Context) authtypes.AccountI { + keepers.Faucet.Fund(ctx, vestingAddr, sdk.NewCoin("other", sdk.NewInt(2))) + return myVestingAccount + }, + expBalances: sdk.NewCoins(sdk.NewCoin("other", sdk.NewInt(2))), + expHandled: true, + }, + "non vesting account - not handled": { + setupAcc: func(t *testing.T, ctx sdk.Context) authtypes.AccountI { + return &authtypes.BaseAccount{Address: myVestingAccount.GetAddress().String()} + }, + expBalances: sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(100))), + expHandled: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + existingAccount := spec.setupAcc(t, ctx) + // overwrite account in store as in keeper before calling prune + keepers.AccountKeeper.SetAccount(ctx, keepers.AccountKeeper.NewAccountWithAddress(ctx, vestingAddr)) + + // when + noGasCtx := ctx.WithGasMeter(sdk.NewGasMeter(0)) // should not use callers gas + gotHandled, gotErr := NewVestingCoinBurner(keepers.BankKeeper).CleanupExistingAccount(noGasCtx, existingAccount) + // then + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expBalances, keepers.BankKeeper.GetAllBalances(ctx, vestingAddr)) + assert.Equal(t, spec.expHandled, gotHandled) + // and no out of gas panic + }) + } +} + +func TestIteratorAllContract(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + example1 := InstantiateHackatomExampleContract(t, ctx, keepers) + example2 := InstantiateHackatomExampleContract(t, ctx, keepers) + example3 := InstantiateHackatomExampleContract(t, ctx, keepers) + example4 := InstantiateHackatomExampleContract(t, ctx, keepers) + + var allContract []string + keepers.WasmKeeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, _ types.ContractInfo) bool { + allContract = append(allContract, addr.String()) + return false + }) + + // IterateContractInfo not ordering + expContracts := []string{example4.Contract.String(), example2.Contract.String(), example1.Contract.String(), example3.Contract.String()} + require.Equal(t, allContract, expContracts) +} + +func TestIteratorContractByCreator(t *testing.T) { + // setup test + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.ContractKeeper + + depositFund := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(parentCtx, creator, depositFund.Add(depositFund...)...) + mockAddress1 := keepers.Faucet.NewFundedRandomAccount(parentCtx, topUp...) + mockAddress2 := keepers.Faucet.NewFundedRandomAccount(parentCtx, topUp...) + mockAddress3 := keepers.Faucet.NewFundedRandomAccount(parentCtx, topUp...) + + contract1ID, _, err := keeper.Create(parentCtx, creator, hackatomWasm, nil) + contract2ID, _, err := keeper.Create(parentCtx, creator, hackatomWasm, nil) + + require.NoError(t, err) + + initMsgBz := HackatomExampleInitMsg{ + Verifier: mockAddress1, + Beneficiary: mockAddress1, + }.GetBytes(t) + + depositContract := sdk.NewCoins(sdk.NewCoin("denom", sdk.NewInt(1_000))) + + gotAddr1, _, _ := keepers.ContractKeeper.Instantiate(parentCtx, contract1ID, mockAddress1, nil, initMsgBz, "label", depositContract) + ctx := parentCtx.WithBlockHeight(parentCtx.BlockHeight() + 1) + gotAddr2, _, _ := keepers.ContractKeeper.Instantiate(ctx, contract1ID, mockAddress2, nil, initMsgBz, "label", depositContract) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + gotAddr3, _, _ := keepers.ContractKeeper.Instantiate(ctx, contract1ID, gotAddr1, nil, initMsgBz, "label", depositContract) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + gotAddr4, _, _ := keepers.ContractKeeper.Instantiate(ctx, contract2ID, mockAddress2, nil, initMsgBz, "label", depositContract) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + gotAddr5, _, _ := keepers.ContractKeeper.Instantiate(ctx, contract2ID, mockAddress2, nil, initMsgBz, "label", depositContract) + + specs := map[string]struct { + creatorAddr sdk.AccAddress + contractsAddr []string + }{ + "single contract": { + creatorAddr: mockAddress1, + contractsAddr: []string{gotAddr1.String()}, + }, + "multiple contracts": { + creatorAddr: mockAddress2, + contractsAddr: []string{gotAddr2.String(), gotAddr4.String(), gotAddr5.String()}, + }, + "contractAdress": { + creatorAddr: gotAddr1, + contractsAddr: []string{gotAddr3.String()}, + }, + "no contracts- unknown": { + creatorAddr: mockAddress3, + contractsAddr: nil, + }, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var allContract []string + keepers.WasmKeeper.IterateContractsByCreator(parentCtx, spec.creatorAddr, func(addr sdk.AccAddress) bool { + allContract = append(allContract, addr.String()) + return false + }) + require.Equal(t, + allContract, + spec.contractsAddr, + ) + }) + } +} + +func TestSetContractAdmin(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities) + k := keepers.WasmKeeper + myAddr := RandomAccountAddress(t) + example := InstantiateReflectExampleContract(t, parentCtx, keepers) + specs := map[string]struct { + newAdmin sdk.AccAddress + caller sdk.AccAddress + policy AuthorizationPolicy + expAdmin string + expErr bool + }{ + "update admin": { + newAdmin: myAddr, + caller: example.CreatorAddr, + policy: DefaultAuthorizationPolicy{}, + expAdmin: myAddr.String(), + }, + "update admin - unauthorized": { + newAdmin: myAddr, + caller: RandomAccountAddress(t), + policy: DefaultAuthorizationPolicy{}, + expErr: true, + }, + "clear admin - default policy": { + caller: example.CreatorAddr, + policy: DefaultAuthorizationPolicy{}, + expAdmin: "", + }, + "clear admin - unauthorized": { + expAdmin: "", + policy: DefaultAuthorizationPolicy{}, + caller: RandomAccountAddress(t), + expErr: true, + }, + "clear admin - gov policy": { + newAdmin: nil, + policy: GovAuthorizationPolicy{}, + caller: example.CreatorAddr, + expAdmin: "", + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + gotErr := k.setContractAdmin(ctx, example.Contract, spec.caller, spec.newAdmin, spec.policy) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expAdmin, k.GetContractInfo(ctx, example.Contract).Admin) + // and event emitted + require.Len(t, em.Events(), 1) + assert.Equal(t, "update_contract_admin", em.Events()[0].Type) + exp := map[string]string{ + "_contract_address": example.Contract.String(), + "new_admin_address": spec.expAdmin, + } + assert.Equal(t, exp, attrsToStringMap(em.Events()[0].Attributes)) + }) + } +} + +func attrsToStringMap(attrs []abci.EventAttribute) map[string]string { + r := make(map[string]string, len(attrs)) + for _, v := range attrs { + r[string(v.Key)] = string(v.Value) + } + return r +} diff --git a/x/wasm/keeper/legacy_querier.go b/x/wasm/keeper/legacy_querier.go new file mode 100644 index 00000000..3bd4e5fc --- /dev/null +++ b/x/wasm/keeper/legacy_querier.go @@ -0,0 +1,154 @@ +package keeper + +import ( + "encoding/json" + "reflect" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +const ( + QueryListContractByCode = "list-contracts-by-code" + QueryGetContract = "contract-info" + QueryGetContractState = "contract-state" + QueryGetCode = "code" + QueryListCode = "list-code" + QueryContractHistory = "contract-history" +) + +const ( + QueryMethodContractStateSmart = "smart" + QueryMethodContractStateAll = "all" + QueryMethodContractStateRaw = "raw" +) + +// NewLegacyQuerier creates a new querier +// Deprecated: the rest support will be removed. You can use the GRPC gateway instead +func NewLegacyQuerier(keeper types.ViewKeeper, gasLimit sdk.Gas) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { + var ( + rsp interface{} + err error + ) + switch path[0] { + case QueryGetContract: + addr, addrErr := sdk.AccAddressFromBech32(path[1]) + if addrErr != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, addrErr.Error()) + } + rsp, err = queryContractInfo(ctx, addr, keeper) + case QueryListContractByCode: + codeID, parseErr := strconv.ParseUint(path[1], 10, 64) + if parseErr != nil { + return nil, sdkerrors.Wrapf(types.ErrInvalid, "code id: %s", parseErr.Error()) + } + rsp = queryContractListByCode(ctx, codeID, keeper) + case QueryGetContractState: + if len(path) < 3 { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown data query endpoint") + } + return queryContractState(ctx, path[1], path[2], req.Data, gasLimit, keeper) + case QueryGetCode: + codeID, parseErr := strconv.ParseUint(path[1], 10, 64) + if parseErr != nil { + return nil, sdkerrors.Wrapf(types.ErrInvalid, "code id: %s", parseErr.Error()) + } + rsp, err = queryCode(ctx, codeID, keeper) + case QueryListCode: + rsp, err = queryCodeList(ctx, keeper) + case QueryContractHistory: + contractAddr, addrErr := sdk.AccAddressFromBech32(path[1]) + if addrErr != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, addrErr.Error()) + } + rsp, err = queryContractHistory(ctx, contractAddr, keeper) + default: + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown data query endpoint") + } + if err != nil { + return nil, err + } + if rsp == nil || reflect.ValueOf(rsp).IsNil() { + return nil, nil + } + bz, err := json.MarshalIndent(rsp, "", " ") + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil + } +} + +func queryContractState(ctx sdk.Context, bech, queryMethod string, data []byte, gasLimit sdk.Gas, keeper types.ViewKeeper) (json.RawMessage, error) { + contractAddr, err := sdk.AccAddressFromBech32(bech) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, bech) + } + + switch queryMethod { + case QueryMethodContractStateAll: + resultData := make([]types.Model, 0) + // this returns a serialized json object (which internally encoded binary fields properly) + keeper.IterateContractState(ctx, contractAddr, func(key, value []byte) bool { + resultData = append(resultData, types.Model{Key: key, Value: value}) + return false + }) + bz, err := json.Marshal(resultData) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil + case QueryMethodContractStateRaw: + // this returns the raw data from the state, base64-encoded + return keeper.QueryRaw(ctx, contractAddr, data), nil + case QueryMethodContractStateSmart: + // we enforce a subjective gas limit on all queries to avoid infinite loops + ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) + msg := types.RawContractMessage(data) + if err := msg.ValidateBasic(); err != nil { + return nil, sdkerrors.Wrap(err, "json msg") + } + // this returns raw bytes (must be base64-encoded) + bz, err := keeper.QuerySmart(ctx, contractAddr, msg) + return bz, err + default: + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, queryMethod) + } +} + +func queryCodeList(ctx sdk.Context, keeper types.ViewKeeper) ([]types.CodeInfoResponse, error) { + var info []types.CodeInfoResponse + keeper.IterateCodeInfos(ctx, func(i uint64, res types.CodeInfo) bool { + info = append(info, types.CodeInfoResponse{ + CodeID: i, + Creator: res.Creator, + DataHash: res.CodeHash, + InstantiatePermission: res.InstantiateConfig, + }) + return false + }) + return info, nil +} + +func queryContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress, keeper types.ViewKeeper) ([]types.ContractCodeHistoryEntry, error) { + history := keeper.GetContractHistory(ctx, contractAddr) + // redact response + for i := range history { + history[i].Updated = nil + } + return history, nil +} + +func queryContractListByCode(ctx sdk.Context, codeID uint64, keeper types.ViewKeeper) []string { + var contracts []string + keeper.IterateContractsByCode(ctx, codeID, func(addr sdk.AccAddress) bool { + contracts = append(contracts, addr.String()) + return false + }) + return contracts +} diff --git a/x/wasm/keeper/legacy_querier_test.go b/x/wasm/keeper/legacy_querier_test.go new file mode 100644 index 00000000..96837001 --- /dev/null +++ b/x/wasm/keeper/legacy_querier_test.go @@ -0,0 +1,364 @@ +package keeper + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestLegacyQueryContractState(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit.Add(deposit...)...) + anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, sdk.NewInt64Coin("denom", 5000)) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: anyAddr, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract to query", deposit) + require.NoError(t, err) + + contractModel := []types.Model{ + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + } + keeper.importContractState(ctx, addr, contractModel) + + // this gets us full error, not redacted sdk.Error + var defaultQueryGasLimit sdk.Gas = 3000000 + q := NewLegacyQuerier(keeper, defaultQueryGasLimit) + + specs := map[string]struct { + srcPath []string + srcReq abci.RequestQuery + // smart and raw queries (not all queries) return raw bytes from contract not []types.Model + // if this is set, then we just compare - (should be json encoded string) + expRes []byte + // if success and expSmartRes is not set, we parse into []types.Model and compare (all state) + expModelLen int + expModelContains []types.Model + expErr error + }{ + "query all": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateAll}, + expModelLen: 3, + expModelContains: []types.Model{ + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + }, + }, + "query raw key": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw}, + srcReq: abci.RequestQuery{Data: []byte("foo")}, + expRes: []byte(`"bar"`), + }, + "query raw binary key": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw}, + srcReq: abci.RequestQuery{Data: []byte{0x0, 0x1}}, + expRes: []byte(`{"count":8}`), + }, + "query smart": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart}, + srcReq: abci.RequestQuery{Data: []byte(`{"verifier":{}}`)}, + expRes: []byte(fmt.Sprintf(`{"verifier":"%s"}`, anyAddr.String())), + }, + "query smart invalid request": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart}, + srcReq: abci.RequestQuery{Data: []byte(`{"raw":{"key":"config"}}`)}, + expErr: types.ErrQueryFailed, + }, + "query smart with invalid json": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart}, + srcReq: abci.RequestQuery{Data: []byte(`not a json string`)}, + expErr: types.ErrInvalid, + }, + "query non-existent raw key": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw}, + srcReq: abci.RequestQuery{Data: []byte("i do not exist")}, + expRes: nil, + }, + "query empty raw key": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw}, + srcReq: abci.RequestQuery{Data: []byte("")}, + expRes: nil, + }, + "query nil raw key": { + srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw}, + srcReq: abci.RequestQuery{Data: nil}, + expRes: nil, + }, + "query raw with unknown address": { + srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateRaw}, + expRes: nil, + }, + "query all with unknown address": { + srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateAll}, + expModelLen: 0, + }, + "query smart with unknown address": { + srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateSmart}, + srcReq: abci.RequestQuery{Data: []byte(`{}`)}, + expModelLen: 0, + expErr: types.ErrNotFound, + }, + } + + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + binResult, err := q(ctx, spec.srcPath, spec.srcReq) + // require.True(t, spec.expErr.Is(err), "unexpected error") + require.True(t, errors.Is(err, spec.expErr), err) + + // if smart query, check custom response + if spec.srcPath[2] != QueryMethodContractStateAll { + require.Equal(t, spec.expRes, binResult) + return + } + + // otherwise, check returned models + var r []types.Model + if spec.expErr == nil { + require.NoError(t, json.Unmarshal(binResult, &r)) + require.NotNil(t, r) + } + require.Len(t, r, spec.expModelLen) + // and in result set + for _, v := range spec.expModelContains { + assert.Contains(t, r, v) + } + }) + } +} + +func TestLegacyQueryContractListByCodeOrdering(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit.Add(deposit...)...) + anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: anyAddr, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + // manage some realistic block settings + var h int64 = 10 + setBlock := func(ctx sdk.Context, height int64) sdk.Context { + ctx = ctx.WithBlockHeight(height) + meter := sdk.NewGasMeter(1000000) + ctx = ctx.WithGasMeter(meter) + ctx = ctx.WithBlockGasMeter(meter) + return ctx + } + + // create 10 contracts with real block/gas setup + for i := range [10]int{} { + // 3 tx per block, so we ensure both comparisons work + if i%3 == 0 { + ctx = setBlock(ctx, h) + h++ + } + _, _, err = keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, fmt.Sprintf("contract %d", i), topUp) + require.NoError(t, err) + } + + // query and check the results are properly sorted + var defaultQueryGasLimit sdk.Gas = 3000000 + q := NewLegacyQuerier(keeper, defaultQueryGasLimit) + + query := []string{QueryListContractByCode, fmt.Sprintf("%d", codeID)} + data := abci.RequestQuery{} + res, err := q(ctx, query, data) + require.NoError(t, err) + + var contracts []string + err = json.Unmarshal(res, &contracts) + require.NoError(t, err) + + require.Equal(t, 10, len(contracts)) + + for _, contract := range contracts { + assert.NotEmpty(t, contract) + } +} + +func TestLegacyQueryContractHistory(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + var otherAddr sdk.AccAddress = bytes.Repeat([]byte{0x2}, types.ContractAddrLen) + + specs := map[string]struct { + srcQueryAddr sdk.AccAddress + srcHistory []types.ContractCodeHistoryEntry + expContent []types.ContractCodeHistoryEntry + }{ + "response with internal fields cleared": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeGenesis, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }}, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeGenesis, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + }}, + }, + "response with multiple entries": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"migrate message 1"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 3, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"migrate message 2"`), + }}, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Msg: []byte(`"migrate message 1"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 3, + Msg: []byte(`"migrate message 2"`), + }}, + }, + "unknown contract address": { + srcQueryAddr: otherAddr, + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeGenesis, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }}, + expContent: []types.ContractCodeHistoryEntry{}, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + _, _, myContractAddr := keyPubAddr() + keeper.appendToContractHistory(ctx, myContractAddr, spec.srcHistory...) + + var defaultQueryGasLimit sdk.Gas = 3000000 + q := NewLegacyQuerier(keeper, defaultQueryGasLimit) + queryContractAddr := spec.srcQueryAddr + if queryContractAddr == nil { + queryContractAddr = myContractAddr + } + + // when + query := []string{QueryContractHistory, queryContractAddr.String()} + data := abci.RequestQuery{} + resData, err := q(ctx, query, data) + + // then + require.NoError(t, err) + var got []types.ContractCodeHistoryEntry + err = json.Unmarshal(resData, &got) + require.NoError(t, err) + + assert.Equal(t, spec.expContent, got) + }) + } +} + +func TestLegacyQueryCodeList(t *testing.T) { + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + specs := map[string]struct { + codeIDs []uint64 + }{ + "none": {}, + "no gaps": { + codeIDs: []uint64{1, 2, 3}, + }, + "with gaps": { + codeIDs: []uint64{2, 4, 6}, + }, + } + + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + for _, codeID := range spec.codeIDs { + require.NoError(t, keeper.importCode(ctx, codeID, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + } + var defaultQueryGasLimit sdk.Gas = 3000000 + q := NewLegacyQuerier(keeper, defaultQueryGasLimit) + // when + query := []string{QueryListCode} + data := abci.RequestQuery{} + resData, err := q(ctx, query, data) + + // then + require.NoError(t, err) + if len(spec.codeIDs) == 0 { + require.Nil(t, resData) + return + } + + var got []map[string]interface{} + err = json.Unmarshal(resData, &got) + require.NoError(t, err) + require.Len(t, got, len(spec.codeIDs)) + for i, exp := range spec.codeIDs { + assert.EqualValues(t, exp, got[i]["id"]) + } + }) + } +} diff --git a/x/wasm/keeper/metrics.go b/x/wasm/keeper/metrics.go new file mode 100644 index 00000000..4c4b959f --- /dev/null +++ b/x/wasm/keeper/metrics.go @@ -0,0 +1,72 @@ +package keeper + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + labelPinned = "pinned" + labelMemory = "memory" + labelFs = "fs" +) + +// metricSource source of wasmvm metrics +type metricSource interface { + GetMetrics() (*wasmvmtypes.Metrics, error) +} + +var _ prometheus.Collector = (*WasmVMMetricsCollector)(nil) + +// WasmVMMetricsCollector custom metrics collector to be used with Prometheus +type WasmVMMetricsCollector struct { + source metricSource + CacheHitsDescr *prometheus.Desc + CacheMissesDescr *prometheus.Desc + CacheElementsDescr *prometheus.Desc + CacheSizeDescr *prometheus.Desc +} + +// NewWasmVMMetricsCollector constructor +func NewWasmVMMetricsCollector(s metricSource) *WasmVMMetricsCollector { + return &WasmVMMetricsCollector{ + source: s, + CacheHitsDescr: prometheus.NewDesc("wasmvm_cache_hits_total", "Total number of cache hits", []string{"type"}, nil), + CacheMissesDescr: prometheus.NewDesc("wasmvm_cache_misses_total", "Total number of cache misses", nil, nil), + CacheElementsDescr: prometheus.NewDesc("wasmvm_cache_elements_total", "Total number of elements in the cache", []string{"type"}, nil), + CacheSizeDescr: prometheus.NewDesc("wasmvm_cache_size_bytes", "Total number of elements in the cache", []string{"type"}, nil), + } +} + +// Register registers all metrics +func (p *WasmVMMetricsCollector) Register(r prometheus.Registerer) { + r.MustRegister(p) +} + +// Describe sends the super-set of all possible descriptors of metrics +func (p *WasmVMMetricsCollector) Describe(descs chan<- *prometheus.Desc) { + descs <- p.CacheHitsDescr + descs <- p.CacheMissesDescr + descs <- p.CacheElementsDescr + descs <- p.CacheSizeDescr +} + +// Collect is called by the Prometheus registry when collecting metrics. +func (p *WasmVMMetricsCollector) Collect(c chan<- prometheus.Metric) { + m, err := p.source.GetMetrics() + if err != nil { + return + } + c <- prometheus.MustNewConstMetric(p.CacheHitsDescr, prometheus.CounterValue, float64(m.HitsPinnedMemoryCache), labelPinned) + c <- prometheus.MustNewConstMetric(p.CacheHitsDescr, prometheus.CounterValue, float64(m.HitsMemoryCache), labelMemory) + c <- prometheus.MustNewConstMetric(p.CacheHitsDescr, prometheus.CounterValue, float64(m.HitsFsCache), labelFs) + c <- prometheus.MustNewConstMetric(p.CacheMissesDescr, prometheus.CounterValue, float64(m.Misses)) + c <- prometheus.MustNewConstMetric(p.CacheElementsDescr, prometheus.GaugeValue, float64(m.ElementsPinnedMemoryCache), labelPinned) + c <- prometheus.MustNewConstMetric(p.CacheElementsDescr, prometheus.GaugeValue, float64(m.ElementsMemoryCache), labelMemory) + c <- prometheus.MustNewConstMetric(p.CacheSizeDescr, prometheus.GaugeValue, float64(m.SizeMemoryCache), labelMemory) + c <- prometheus.MustNewConstMetric(p.CacheSizeDescr, prometheus.GaugeValue, float64(m.SizePinnedMemoryCache), labelPinned) + // Node about fs metrics: + // The number of elements and the size of elements in the file system cache cannot easily be obtained. + // We had to either scan the whole directory of potentially thousands of files or track the values when files are added or removed. + // Such a tracking would need to be on disk such that the values are not cleared when the node is restarted. +} diff --git a/x/wasm/keeper/migrate_test.go b/x/wasm/keeper/migrate_test.go new file mode 100644 index 00000000..fd7a437b --- /dev/null +++ b/x/wasm/keeper/migrate_test.go @@ -0,0 +1,61 @@ +package keeper + +import ( + "bytes" + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestMigrate1To2(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + wasmKeeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := sdk.AccAddress(bytes.Repeat([]byte{1}, address.Len)) + keepers.Faucet.Fund(ctx, creator, deposit...) + example := StoreHackatomExampleContract(t, ctx, keepers) + + initMsg := HackatomExampleInitMsg{ + Verifier: RandomAccountAddress(t), + Beneficiary: RandomAccountAddress(t), + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + em := sdk.NewEventManager() + + // create with no balance is also legal + gotContractAddr1, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + gotContractAddr2, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + gotContractAddr3, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), example.CodeID, creator, nil, initMsgBz, "demo contract 1", nil) + + info1 := wasmKeeper.GetContractInfo(ctx, gotContractAddr1) + info2 := wasmKeeper.GetContractInfo(ctx, gotContractAddr2) + info3 := wasmKeeper.GetContractInfo(ctx, gotContractAddr3) + + // remove key + ctx.KVStore(wasmKeeper.storeKey).Delete(types.GetContractByCreatorSecondaryIndexKey(creator, info1.Created.Bytes(), gotContractAddr1)) + ctx.KVStore(wasmKeeper.storeKey).Delete(types.GetContractByCreatorSecondaryIndexKey(creator, info2.Created.Bytes(), gotContractAddr2)) + ctx.KVStore(wasmKeeper.storeKey).Delete(types.GetContractByCreatorSecondaryIndexKey(creator, info3.Created.Bytes(), gotContractAddr3)) + + // migrator + migrator := NewMigrator(*wasmKeeper) + migrator.Migrate1to2(ctx) + + // check new store + var allContract []string + wasmKeeper.IterateContractsByCreator(ctx, creator, func(addr sdk.AccAddress) bool { + allContract = append(allContract, addr.String()) + return false + }) + + require.Equal(t, []string{gotContractAddr1.String(), gotContractAddr2.String(), gotContractAddr3.String()}, allContract) +} diff --git a/x/wasm/keeper/migrations.go b/x/wasm/keeper/migrations.go new file mode 100644 index 00000000..f8d375d0 --- /dev/null +++ b/x/wasm/keeper/migrations.go @@ -0,0 +1,27 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// Migrator is a struct for handling in-place store migrations. +type Migrator struct { + keeper Keeper +} + +// NewMigrator returns a new Migrator. +func NewMigrator(keeper Keeper) Migrator { + return Migrator{keeper: keeper} +} + +// Migrate1to2 migrates from version 1 to 2. +func (m Migrator) Migrate1to2(ctx sdk.Context) error { + m.keeper.IterateContractInfo(ctx, func(contractAddr sdk.AccAddress, contractInfo types.ContractInfo) bool { + creator := sdk.MustAccAddressFromBech32(contractInfo.Creator) + m.keeper.addToContractCreatorSecondaryIndex(ctx, creator, contractInfo.Created, contractAddr) + return false + }) + return nil +} diff --git a/x/wasm/keeper/msg_dispatcher.go b/x/wasm/keeper/msg_dispatcher.go new file mode 100644 index 00000000..07d3aab9 --- /dev/null +++ b/x/wasm/keeper/msg_dispatcher.go @@ -0,0 +1,222 @@ +package keeper + +import ( + "bytes" + "fmt" + "sort" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// Messenger is an extension point for custom wasmd message handling +type Messenger interface { + // DispatchMsg encodes the wasmVM message and dispatches it. + DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) +} + +// replyer is a subset of keeper that can handle replies to submessages +type replyer interface { + reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) +} + +// MessageDispatcher coordinates message sending and submessage reply/ state commits +type MessageDispatcher struct { + messenger Messenger + keeper replyer +} + +// NewMessageDispatcher constructor +func NewMessageDispatcher(messenger Messenger, keeper replyer) *MessageDispatcher { + return &MessageDispatcher{messenger: messenger, keeper: keeper} +} + +// DispatchMessages sends all messages. +func (d MessageDispatcher) DispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error { + for _, msg := range msgs { + events, _, err := d.messenger.DispatchMsg(ctx, contractAddr, ibcPort, msg) + if err != nil { + return err + } + // redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler) + ctx.EventManager().EmitEvents(events) + } + return nil +} + +// dispatchMsgWithGasLimit sends a message with gas limit applied +func (d MessageDispatcher) dispatchMsgWithGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msg wasmvmtypes.CosmosMsg, gasLimit uint64) (events []sdk.Event, data [][]byte, err error) { + limitedMeter := sdk.NewGasMeter(gasLimit) + subCtx := ctx.WithGasMeter(limitedMeter) + + // catch out of gas panic and just charge the entire gas limit + defer func() { + if r := recover(); r != nil { + // if it's not an OutOfGas error, raise it again + if _, ok := r.(sdk.ErrorOutOfGas); !ok { + // log it to get the original stack trace somewhere (as panic(r) keeps message but stacktrace to here + moduleLogger(ctx).Info("SubMsg rethrowing panic: %#v", r) + panic(r) + } + ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic") + err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit") + } + }() + events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg) + + // make sure we charge the parent what was spent + spent := subCtx.GasMeter().GasConsumed() + ctx.GasMeter().ConsumeGas(spent, "From limited Sub-Message") + + return events, data, err +} + +// DispatchSubmessages builds a sandbox to execute these messages and returns the execution result to the contract +// that dispatched them, both on success as well as failure +func (d MessageDispatcher) DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + var rsp []byte + for _, msg := range msgs { + switch msg.ReplyOn { + case wasmvmtypes.ReplySuccess, wasmvmtypes.ReplyError, wasmvmtypes.ReplyAlways, wasmvmtypes.ReplyNever: + default: + return nil, sdkerrors.Wrap(types.ErrInvalid, "replyOn value") + } + // first, we build a sub-context which we can use inside the submessages + subCtx, commit := ctx.CacheContext() + em := sdk.NewEventManager() + subCtx = subCtx.WithEventManager(em) + + // check how much gas left locally, optionally wrap the gas meter + gasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumed() + limitGas := msg.GasLimit != nil && (*msg.GasLimit < gasRemaining) + + var err error + var events []sdk.Event + var data [][]byte + if limitGas { + events, data, err = d.dispatchMsgWithGasLimit(subCtx, contractAddr, ibcPort, msg.Msg, *msg.GasLimit) + } else { + events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg.Msg) + } + + // if it succeeds, commit state changes from submessage, and pass on events to Event Manager + var filteredEvents []sdk.Event + if err == nil { + commit() + filteredEvents = filterEvents(append(em.Events(), events...)) + ctx.EventManager().EmitEvents(filteredEvents) + if msg.Msg.Wasm == nil { + filteredEvents = []sdk.Event{} + } else { + for _, e := range filteredEvents { + attributes := e.Attributes + sort.SliceStable(attributes, func(i, j int) bool { + return bytes.Compare(attributes[i].Key, attributes[j].Key) < 0 + }) + } + } + } // on failure, revert state from sandbox, and ignore events (just skip doing the above) + + // we only callback if requested. Short-circuit here the cases we don't want to + if (msg.ReplyOn == wasmvmtypes.ReplySuccess || msg.ReplyOn == wasmvmtypes.ReplyNever) && err != nil { + return nil, err + } + if msg.ReplyOn == wasmvmtypes.ReplyNever || (msg.ReplyOn == wasmvmtypes.ReplyError && err == nil) { + continue + } + + // otherwise, we create a SubMsgResult and pass it into the calling contract + var result wasmvmtypes.SubMsgResult + if err == nil { + // just take the first one for now if there are multiple sub-sdk messages + // and safely return nothing if no data + var responseData []byte + if len(data) > 0 { + responseData = data[0] + } + result = wasmvmtypes.SubMsgResult{ + Ok: &wasmvmtypes.SubMsgResponse{ + Events: sdkEventsToWasmVMEvents(filteredEvents), + Data: responseData, + }, + } + } else { + // Issue #759 - we don't return error string for worries of non-determinism + moduleLogger(ctx).Info("Redacting submessage error", "cause", err) + result = wasmvmtypes.SubMsgResult{ + Err: redactError(err).Error(), + } + } + + // now handle the reply, we use the parent context, and abort on error + reply := wasmvmtypes.Reply{ + ID: msg.ID, + Result: result, + } + + // we can ignore any result returned as there is nothing to do with the data + // and the events are already in the ctx.EventManager() + rspData, err := d.keeper.reply(ctx, contractAddr, reply) + switch { + case err != nil: + return nil, sdkerrors.Wrap(err, "reply") + case rspData != nil: + rsp = rspData + } + } + return rsp, nil +} + +// Issue #759 - we don't return error string for worries of non-determinism +func redactError(err error) error { + // Do not redact system errors + // SystemErrors must be created in x/wasm and we can ensure determinism + if wasmvmtypes.ToSystemError(err) != nil { + return err + } + + // FIXME: do we want to hardcode some constant string mappings here as well? + // Or better document them? (SDK error string may change on a patch release to fix wording) + // sdk/11 is out of gas + // sdk/5 is insufficient funds (on bank send) + // (we can theoretically redact less in the future, but this is a first step to safety) + codespace, code, _ := sdkerrors.ABCIInfo(err, false) + return fmt.Errorf("codespace: %s, code: %d", codespace, code) +} + +func filterEvents(events []sdk.Event) []sdk.Event { + // pre-allocate space for efficiency + res := make([]sdk.Event, 0, len(events)) + for _, ev := range events { + if ev.Type != "message" { + res = append(res, ev) + } + } + return res +} + +func sdkEventsToWasmVMEvents(events []sdk.Event) []wasmvmtypes.Event { + res := make([]wasmvmtypes.Event, len(events)) + for i, ev := range events { + res[i] = wasmvmtypes.Event{ + Type: ev.Type, + Attributes: sdkAttributesToWasmVMAttributes(ev.Attributes), + } + } + return res +} + +func sdkAttributesToWasmVMAttributes(attrs []abci.EventAttribute) []wasmvmtypes.EventAttribute { + res := make([]wasmvmtypes.EventAttribute, len(attrs)) + for i, attr := range attrs { + res[i] = wasmvmtypes.EventAttribute{ + Key: string(attr.Key), + Value: string(attr.Value), + } + } + return res +} diff --git a/x/wasm/keeper/msg_dispatcher_test.go b/x/wasm/keeper/msg_dispatcher_test.go new file mode 100644 index 00000000..0e9db4e0 --- /dev/null +++ b/x/wasm/keeper/msg_dispatcher_test.go @@ -0,0 +1,426 @@ +package keeper + +import ( + "errors" + "fmt" + "testing" + + "github.com/tendermint/tendermint/libs/log" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" +) + +func TestDispatchSubmessages(t *testing.T) { + noReplyCalled := &mockReplyer{} + var anyGasLimit uint64 = 1 + specs := map[string]struct { + msgs []wasmvmtypes.SubMsg + replyer *mockReplyer + msgHandler *wasmtesting.MockMessageHandler + expErr bool + expData []byte + expCommits []bool + expEvents sdk.Events + }{ + "no reply on error without error": { + msgs: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyError}}, + replyer: noReplyCalled, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, [][]byte{[]byte("myData")}, nil + }, + }, + expCommits: []bool{true}, + }, + "no reply on success without success": { + msgs: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplySuccess}}, + replyer: noReplyCalled, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, errors.New("test, ignore") + }, + }, + expCommits: []bool{false}, + expErr: true, + }, + "reply on success - received": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplySuccess, + }}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + return []byte("myReplyData"), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, [][]byte{[]byte("myData")}, nil + }, + }, + expData: []byte("myReplyData"), + expCommits: []bool{true}, + }, + "reply on error - handled": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplyError, + }}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + return []byte("myReplyData"), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, errors.New("my error") + }, + }, + expData: []byte("myReplyData"), + expCommits: []bool{false}, + }, + "with reply events": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplySuccess, + }}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + ctx.EventManager().EmitEvent(sdk.NewEvent("wasm-reply")) + return []byte("myReplyData"), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}} + return myEvents, [][]byte{[]byte("myData")}, nil + }, + }, + expData: []byte("myReplyData"), + expCommits: []bool{true}, + expEvents: []sdk.Event{ + { + Type: "myEvent", + Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}, + }, + sdk.NewEvent("wasm-reply"), + }, + }, + "with context events - released on commit": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplyNever, + }}, + replyer: &mockReplyer{}, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}} + ctx.EventManager().EmitEvents(myEvents) + return nil, nil, nil + }, + }, + expCommits: []bool{true}, + expEvents: []sdk.Event{{ + Type: "myEvent", + Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}, + }}, + }, + "with context events - discarded on failure": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplyNever, + }}, + replyer: &mockReplyer{}, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}} + ctx.EventManager().EmitEvents(myEvents) + return nil, nil, errors.New("testing") + }, + }, + expCommits: []bool{false}, + expErr: true, + }, + "reply returns error": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplySuccess, + }}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + return nil, errors.New("reply failed") + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, nil + }, + }, + expCommits: []bool{false}, + expErr: true, + }, + "with gas limit - out of gas": { + msgs: []wasmvmtypes.SubMsg{{ + GasLimit: &anyGasLimit, + ReplyOn: wasmvmtypes.ReplyError, + }}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + return []byte("myReplyData"), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + ctx.GasMeter().ConsumeGas(sdk.Gas(101), "testing") + return nil, [][]byte{[]byte("someData")}, nil + }, + }, + expData: []byte("myReplyData"), + expCommits: []bool{false}, + }, + "with gas limit - within limit no error": { + msgs: []wasmvmtypes.SubMsg{{ + GasLimit: &anyGasLimit, + ReplyOn: wasmvmtypes.ReplyError, + }}, + replyer: &mockReplyer{}, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + ctx.GasMeter().ConsumeGas(sdk.Gas(1), "testing") + return nil, [][]byte{[]byte("someData")}, nil + }, + }, + expCommits: []bool{true}, + }, + "never reply - with nil response": { + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyNever}, {ID: 2, ReplyOn: wasmvmtypes.ReplyNever}}, + replyer: &mockReplyer{}, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, [][]byte{nil}, nil + }, + }, + expCommits: []bool{true, true}, + }, + "never reply - with any non nil response": { + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyNever}, {ID: 2, ReplyOn: wasmvmtypes.ReplyNever}}, + replyer: &mockReplyer{}, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, [][]byte{{}}, nil + }, + }, + expCommits: []bool{true, true}, + }, + "never reply - with error": { + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyNever}, {ID: 2, ReplyOn: wasmvmtypes.ReplyNever}}, + replyer: &mockReplyer{}, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, [][]byte{{}}, errors.New("testing") + }, + }, + expCommits: []bool{false, false}, + expErr: true, + }, + "multiple msg - last reply returned": { + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + return []byte(fmt.Sprintf("myReplyData:%d", reply.ID)), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, errors.New("my error") + }, + }, + expData: []byte("myReplyData:2"), + expCommits: []bool{false, false}, + }, + "multiple msg - last non nil reply returned": { + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + if reply.ID == 2 { + return nil, nil + } + return []byte("myReplyData:1"), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, errors.New("my error") + }, + }, + expData: []byte("myReplyData:1"), + expCommits: []bool{false, false}, + }, + "multiple msg - empty reply can overwrite result": { + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + if reply.ID == 2 { + return []byte{}, nil + } + return []byte("myReplyData:1"), nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, errors.New("my error") + }, + }, + expData: []byte{}, + expCommits: []bool{false, false}, + }, + "message event filtered without reply": { + msgs: []wasmvmtypes.SubMsg{{ + ReplyOn: wasmvmtypes.ReplyNever, + }}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + return nil, errors.New("should never be called") + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + myEvents := []sdk.Event{ + sdk.NewEvent("message"), + sdk.NewEvent("execute", sdk.NewAttribute("foo", "bar")), + } + return myEvents, [][]byte{[]byte("myData")}, nil + }, + }, + expData: nil, + expCommits: []bool{true}, + expEvents: []sdk.Event{sdk.NewEvent("execute", sdk.NewAttribute("foo", "bar"))}, + }, + "wasm reply gets proper events": { + // put fake wasmmsg in here to show where it comes from + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyAlways, Msg: wasmvmtypes.CosmosMsg{Wasm: &wasmvmtypes.WasmMsg{}}}}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + if reply.Result.Err != "" { + return nil, errors.New(reply.Result.Err) + } + res := reply.Result.Ok + + // ensure the input events are what we expect + // I didn't use require.Equal() to act more like a contract... but maybe that would be better + if len(res.Events) != 2 { + return nil, fmt.Errorf("event count: %#v", res.Events) + } + if res.Events[0].Type != "execute" { + return nil, fmt.Errorf("event0: %#v", res.Events[0]) + } + if res.Events[1].Type != "wasm" { + return nil, fmt.Errorf("event1: %#v", res.Events[1]) + } + + // let's add a custom event here and see if it makes it out + ctx.EventManager().EmitEvent(sdk.NewEvent("wasm-reply")) + + // update data from what we got in + return res.Data, nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + events = []sdk.Event{ + sdk.NewEvent("message", sdk.NewAttribute("_contract_address", contractAddr.String())), + // we don't know what the contarctAddr will be so we can't use it in the final tests + sdk.NewEvent("execute", sdk.NewAttribute("_contract_address", "placeholder-random-addr")), + sdk.NewEvent("wasm", sdk.NewAttribute("random", "data")), + } + return events, [][]byte{[]byte("subData")}, nil + }, + }, + expData: []byte("subData"), + expCommits: []bool{true}, + expEvents: []sdk.Event{ + sdk.NewEvent("execute", sdk.NewAttribute("_contract_address", "placeholder-random-addr")), + sdk.NewEvent("wasm", sdk.NewAttribute("random", "data")), + sdk.NewEvent("wasm-reply"), + }, + }, + "non-wasm reply events get filtered": { + // show events from a stargate message gets filtered out + msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyAlways, Msg: wasmvmtypes.CosmosMsg{Stargate: &wasmvmtypes.StargateMsg{}}}}, + replyer: &mockReplyer{ + replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + if reply.Result.Err != "" { + return nil, errors.New(reply.Result.Err) + } + res := reply.Result.Ok + + // ensure the input events are what we expect + // I didn't use require.Equal() to act more like a contract... but maybe that would be better + if len(res.Events) != 0 { + return nil, errors.New("events not filtered out") + } + + // let's add a custom event here and see if it makes it out + ctx.EventManager().EmitEvent(sdk.NewEvent("stargate-reply")) + + // update data from what we got in + return res.Data, nil + }, + }, + msgHandler: &wasmtesting.MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + events = []sdk.Event{ + // this is filtered out + sdk.NewEvent("message", sdk.NewAttribute("stargate", "something-something")), + // we still emit this to the client, but not the contract + sdk.NewEvent("non-determinstic"), + } + return events, [][]byte{[]byte("subData")}, nil + }, + }, + expData: []byte("subData"), + expCommits: []bool{true}, + expEvents: []sdk.Event{ + sdk.NewEvent("non-determinstic"), + // the event from reply is also exposed + sdk.NewEvent("stargate-reply"), + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var mockStore wasmtesting.MockCommitMultiStore + em := sdk.NewEventManager() + ctx := sdk.Context{}.WithMultiStore(&mockStore). + WithGasMeter(sdk.NewGasMeter(100)). + WithEventManager(em).WithLogger(log.TestingLogger()) + d := NewMessageDispatcher(spec.msgHandler, spec.replyer) + gotData, gotErr := d.DispatchSubmessages(ctx, RandomAccountAddress(t), "any_port", spec.msgs) + if spec.expErr { + require.Error(t, gotErr) + assert.Empty(t, em.Events()) + return + } else { + require.NoError(t, gotErr) + assert.Equal(t, spec.expData, gotData) + } + assert.Equal(t, spec.expCommits, mockStore.Committed) + if len(spec.expEvents) == 0 { + assert.Empty(t, em.Events()) + } else { + assert.Equal(t, spec.expEvents, em.Events()) + } + }) + } +} + +type mockReplyer struct { + replyFn func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) +} + +func (m mockReplyer) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) ([]byte, error) { + if m.replyFn == nil { + panic("not expected to be called") + } + return m.replyFn(ctx, contractAddress, reply) +} diff --git a/x/wasm/keeper/msg_server.go b/x/wasm/keeper/msg_server.go new file mode 100644 index 00000000..3034b929 --- /dev/null +++ b/x/wasm/keeper/msg_server.go @@ -0,0 +1,256 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ types.MsgServer = msgServer{} + +type msgServer struct { + keeper types.ContractOpsKeeper +} + +func NewMsgServerImpl(k types.ContractOpsKeeper) types.MsgServer { + return &msgServer{keeper: k} +} + +func (m msgServer) StoreCode(goCtx context.Context, msg *types.MsgStoreCode) (*types.MsgStoreCodeResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + ctx := sdk.UnwrapSDKContext(goCtx) + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + codeID, checksum, err := m.keeper.Create(ctx, senderAddr, msg.WASMByteCode, msg.InstantiatePermission) + if err != nil { + return nil, err + } + + return &types.MsgStoreCodeResponse{ + CodeID: codeID, + Checksum: checksum, + }, nil +} + +// InstantiateContract instantiate a new contract with classic sequence based address generation +func (m msgServer) InstantiateContract(goCtx context.Context, msg *types.MsgInstantiateContract) (*types.MsgInstantiateContractResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + ctx := sdk.UnwrapSDKContext(goCtx) + + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + var adminAddr sdk.AccAddress + if msg.Admin != "" { + if adminAddr, err = sdk.AccAddressFromBech32(msg.Admin); err != nil { + return nil, sdkerrors.Wrap(err, "admin") + } + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + contractAddr, data, err := m.keeper.Instantiate(ctx, msg.CodeID, senderAddr, adminAddr, msg.Msg, msg.Label, msg.Funds) + if err != nil { + return nil, err + } + + return &types.MsgInstantiateContractResponse{ + Address: contractAddr.String(), + Data: data, + }, nil +} + +// InstantiateContract2 instantiate a new contract with predicatable address generated +func (m msgServer) InstantiateContract2(goCtx context.Context, msg *types.MsgInstantiateContract2) (*types.MsgInstantiateContract2Response, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + ctx := sdk.UnwrapSDKContext(goCtx) + + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + var adminAddr sdk.AccAddress + if msg.Admin != "" { + if adminAddr, err = sdk.AccAddressFromBech32(msg.Admin); err != nil { + return nil, sdkerrors.Wrap(err, "admin") + } + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + contractAddr, data, err := m.keeper.Instantiate2(ctx, msg.CodeID, senderAddr, adminAddr, msg.Msg, msg.Label, msg.Funds, msg.Salt, msg.FixMsg) + if err != nil { + return nil, err + } + + return &types.MsgInstantiateContract2Response{ + Address: contractAddr.String(), + Data: data, + }, nil +} + +func (m msgServer) ExecuteContract(goCtx context.Context, msg *types.MsgExecuteContract) (*types.MsgExecuteContractResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + contractAddr, err := sdk.AccAddressFromBech32(msg.Contract) + if err != nil { + return nil, sdkerrors.Wrap(err, "contract") + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + data, err := m.keeper.Execute(ctx, contractAddr, senderAddr, msg.Msg, msg.Funds) + if err != nil { + return nil, err + } + + return &types.MsgExecuteContractResponse{ + Data: data, + }, nil +} + +func (m msgServer) MigrateContract(goCtx context.Context, msg *types.MsgMigrateContract) (*types.MsgMigrateContractResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + contractAddr, err := sdk.AccAddressFromBech32(msg.Contract) + if err != nil { + return nil, sdkerrors.Wrap(err, "contract") + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + data, err := m.keeper.Migrate(ctx, contractAddr, senderAddr, msg.CodeID, msg.Msg) + if err != nil { + return nil, err + } + + return &types.MsgMigrateContractResponse{ + Data: data, + }, nil +} + +func (m msgServer) UpdateAdmin(goCtx context.Context, msg *types.MsgUpdateAdmin) (*types.MsgUpdateAdminResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + contractAddr, err := sdk.AccAddressFromBech32(msg.Contract) + if err != nil { + return nil, sdkerrors.Wrap(err, "contract") + } + newAdminAddr, err := sdk.AccAddressFromBech32(msg.NewAdmin) + if err != nil { + return nil, sdkerrors.Wrap(err, "new admin") + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + if err := m.keeper.UpdateContractAdmin(ctx, contractAddr, senderAddr, newAdminAddr); err != nil { + return nil, err + } + + return &types.MsgUpdateAdminResponse{}, nil +} + +func (m msgServer) ClearAdmin(goCtx context.Context, msg *types.MsgClearAdmin) (*types.MsgClearAdminResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + contractAddr, err := sdk.AccAddressFromBech32(msg.Contract) + if err != nil { + return nil, sdkerrors.Wrap(err, "contract") + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + if err := m.keeper.ClearContractAdmin(ctx, contractAddr, senderAddr); err != nil { + return nil, err + } + + return &types.MsgClearAdminResponse{}, nil +} + +func (m msgServer) UpdateInstantiateConfig(goCtx context.Context, msg *types.MsgUpdateInstantiateConfig) (*types.MsgUpdateInstantiateConfigResponse, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + if err := m.keeper.SetAccessConfig(ctx, msg.CodeID, sdk.AccAddress(msg.Sender), *msg.NewInstantiatePermission); err != nil { + return nil, err + } + ctx.EventManager().EmitEvent(sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + )) + + return &types.MsgUpdateInstantiateConfigResponse{}, nil +} diff --git a/x/wasm/keeper/msg_server_integration_test.go b/x/wasm/keeper/msg_server_integration_test.go new file mode 100644 index 00000000..c5dd0b21 --- /dev/null +++ b/x/wasm/keeper/msg_server_integration_test.go @@ -0,0 +1,46 @@ +package keeper_test + +import ( + "crypto/sha256" + _ "embed" + "testing" + + "github.com/cosmos/cosmos-sdk/testutil/testdata" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cerc-io/laconicd/app" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +//go:embed testdata/reflect.wasm +var wasmContract []byte + +func TestStoreCode(t *testing.T) { + wasmApp := app.Setup(false) + ctx := wasmApp.BaseApp.NewContext(false, tmproto.Header{}) + _, _, sender := testdata.KeyTestPubAddr() + msg := types.MsgStoreCodeFixture(func(m *types.MsgStoreCode) { + m.WASMByteCode = wasmContract + m.Sender = sender.String() + }) + + // when + rsp, err := wasmApp.MsgServiceRouter().Handler(msg)(ctx, msg) + + // then + require.NoError(t, err) + var result types.MsgStoreCodeResponse + require.NoError(t, wasmApp.AppCodec().Unmarshal(rsp.Data, &result)) + assert.Equal(t, uint64(1), result.CodeID) + expHash := sha256.Sum256(wasmContract) + assert.Equal(t, expHash[:], result.Checksum) + // and + info := wasmApp.WasmKeeper.GetCodeInfo(ctx, 1) + assert.NotNil(t, info) + assert.Equal(t, expHash[:], info.CodeHash) + assert.Equal(t, sender.String(), info.Creator) + assert.Equal(t, types.DefaultParams().InstantiateDefaultPermission.With(sender), info.InstantiateConfig) +} diff --git a/x/wasm/keeper/options.go b/x/wasm/keeper/options.go new file mode 100644 index 00000000..3136fd00 --- /dev/null +++ b/x/wasm/keeper/options.go @@ -0,0 +1,170 @@ +package keeper + +import ( + "fmt" + "reflect" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/prometheus/client_golang/prometheus" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +type optsFn func(*Keeper) + +func (f optsFn) apply(keeper *Keeper) { + f(keeper) +} + +// WithWasmEngine is an optional constructor parameter to replace the default wasmVM engine with the +// given one. +func WithWasmEngine(x types.WasmerEngine) Option { + return optsFn(func(k *Keeper) { + k.wasmVM = x + }) +} + +// WithMessageHandler is an optional constructor parameter to set a custom handler for wasmVM messages. +// This option should not be combined with Option `WithMessageEncoders` or `WithMessageHandlerDecorator` +func WithMessageHandler(x Messenger) Option { + return optsFn(func(k *Keeper) { + k.messenger = x + }) +} + +// WithMessageHandlerDecorator is an optional constructor parameter to decorate the wasm handler for wasmVM messages. +// This option should not be combined with Option `WithMessageEncoders` or `WithMessageHandler` +func WithMessageHandlerDecorator(d func(old Messenger) Messenger) Option { + return optsFn(func(k *Keeper) { + k.messenger = d(k.messenger) + }) +} + +// WithQueryHandler is an optional constructor parameter to set custom query handler for wasmVM requests. +// This option should not be combined with Option `WithQueryPlugins` or `WithQueryHandlerDecorator` +func WithQueryHandler(x WasmVMQueryHandler) Option { + return optsFn(func(k *Keeper) { + k.wasmVMQueryHandler = x + }) +} + +// WithQueryHandlerDecorator is an optional constructor parameter to decorate the default wasm query handler for wasmVM requests. +// This option should not be combined with Option `WithQueryPlugins` or `WithQueryHandler` +func WithQueryHandlerDecorator(d func(old WasmVMQueryHandler) WasmVMQueryHandler) Option { + return optsFn(func(k *Keeper) { + k.wasmVMQueryHandler = d(k.wasmVMQueryHandler) + }) +} + +// WithQueryPlugins is an optional constructor parameter to pass custom query plugins for wasmVM requests. +// This option expects the default `QueryHandler` set and should not be combined with Option `WithQueryHandler` or `WithQueryHandlerDecorator`. +func WithQueryPlugins(x *QueryPlugins) Option { + return optsFn(func(k *Keeper) { + q, ok := k.wasmVMQueryHandler.(QueryPlugins) + if !ok { + panic(fmt.Sprintf("Unsupported query handler type: %T", k.wasmVMQueryHandler)) + } + k.wasmVMQueryHandler = q.Merge(x) + }) +} + +// WithMessageEncoders is an optional constructor parameter to pass custom message encoder to the default wasm message handler. +// This option expects the `DefaultMessageHandler` set and should not be combined with Option `WithMessageHandler` or `WithMessageHandlerDecorator`. +func WithMessageEncoders(x *MessageEncoders) Option { + return optsFn(func(k *Keeper) { + q, ok := k.messenger.(*MessageHandlerChain) + if !ok { + panic(fmt.Sprintf("Unsupported message handler type: %T", k.messenger)) + } + s, ok := q.handlers[0].(SDKMessageHandler) + if !ok { + panic(fmt.Sprintf("Unexpected message handler type: %T", q.handlers[0])) + } + e, ok := s.encoders.(MessageEncoders) + if !ok { + panic(fmt.Sprintf("Unsupported encoder type: %T", s.encoders)) + } + s.encoders = e.Merge(x) + q.handlers[0] = s + }) +} + +// WithCoinTransferrer is an optional constructor parameter to set a custom coin transferrer +func WithCoinTransferrer(x CoinTransferrer) Option { + if x == nil { + panic("must not be nil") + } + return optsFn(func(k *Keeper) { + k.bank = x + }) +} + +// WithAccountPruner is an optional constructor parameter to set a custom type that handles balances and data cleanup +// for accounts pruned on contract instantiate +func WithAccountPruner(x AccountPruner) Option { + if x == nil { + panic("must not be nil") + } + return optsFn(func(k *Keeper) { + k.accountPruner = x + }) +} + +func WithVMCacheMetrics(r prometheus.Registerer) Option { + return optsFn(func(k *Keeper) { + NewWasmVMMetricsCollector(k.wasmVM).Register(r) + }) +} + +// WithGasRegister set a new gas register to implement custom gas costs. +// When the "gas multiplier" for wasmvm gas conversion is modified inside the new register, +// make sure to also use `WithApiCosts` option for non default values +func WithGasRegister(x GasRegister) Option { + if x == nil { + panic("must not be nil") + } + return optsFn(func(k *Keeper) { + k.gasRegister = x + }) +} + +// WithAPICosts sets custom api costs. Amounts are in cosmwasm gas Not SDK gas. +func WithAPICosts(human, canonical uint64) Option { + return optsFn(func(_ *Keeper) { + costHumanize = human + costCanonical = canonical + }) +} + +// WithMaxQueryStackSize overwrites the default limit for maximum query stacks +func WithMaxQueryStackSize(m uint32) Option { + return optsFn(func(k *Keeper) { + k.maxQueryStackSize = m + }) +} + +// WithAcceptedAccountTypesOnContractInstantiation sets the accepted account types. Account types of this list won't be overwritten or cause a failure +// when they exist for an address on contract instantiation. +// +// Values should be references and contain the `*authtypes.BaseAccount` as default bank account type. +func WithAcceptedAccountTypesOnContractInstantiation(accts ...authtypes.AccountI) Option { + m := asTypeMap(accts) + return optsFn(func(k *Keeper) { + k.acceptedAccountTypes = m + }) +} + +func asTypeMap(accts []authtypes.AccountI) map[reflect.Type]struct{} { + m := make(map[reflect.Type]struct{}, len(accts)) + for _, a := range accts { + if a == nil { + panic(types.ErrEmpty.Wrap("address")) + } + at := reflect.TypeOf(a) + if _, exists := m[at]; exists { + panic(types.ErrDuplicate.Wrapf("%T", a)) + } + m[at] = struct{}{} + } + return m +} diff --git a/x/wasm/keeper/options_test.go b/x/wasm/keeper/options_test.go new file mode 100644 index 00000000..47e9e8c2 --- /dev/null +++ b/x/wasm/keeper/options_test.go @@ -0,0 +1,116 @@ +package keeper + +import ( + "reflect" + "testing" + + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestConstructorOptions(t *testing.T) { + specs := map[string]struct { + srcOpt Option + verify func(*testing.T, Keeper) + }{ + "wasm engine": { + srcOpt: WithWasmEngine(&wasmtesting.MockWasmer{}), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockWasmer{}, k.wasmVM) + }, + }, + "message handler": { + srcOpt: WithMessageHandler(&wasmtesting.MockMessageHandler{}), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockMessageHandler{}, k.messenger) + }, + }, + "query plugins": { + srcOpt: WithQueryHandler(&wasmtesting.MockQueryHandler{}), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockQueryHandler{}, k.wasmVMQueryHandler) + }, + }, + "message handler decorator": { + srcOpt: WithMessageHandlerDecorator(func(old Messenger) Messenger { + require.IsType(t, &MessageHandlerChain{}, old) + return &wasmtesting.MockMessageHandler{} + }), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockMessageHandler{}, k.messenger) + }, + }, + "query plugins decorator": { + srcOpt: WithQueryHandlerDecorator(func(old WasmVMQueryHandler) WasmVMQueryHandler { + require.IsType(t, QueryPlugins{}, old) + return &wasmtesting.MockQueryHandler{} + }), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockQueryHandler{}, k.wasmVMQueryHandler) + }, + }, + "coin transferrer": { + srcOpt: WithCoinTransferrer(&wasmtesting.MockCoinTransferrer{}), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockCoinTransferrer{}, k.bank) + }, + }, + "costs": { + srcOpt: WithGasRegister(&wasmtesting.MockGasRegister{}), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, &wasmtesting.MockGasRegister{}, k.gasRegister) + }, + }, + "api costs": { + srcOpt: WithAPICosts(1, 2), + verify: func(t *testing.T, k Keeper) { + t.Cleanup(setApiDefaults) + assert.Equal(t, uint64(1), costHumanize) + assert.Equal(t, uint64(2), costCanonical) + }, + }, + "max recursion query limit": { + srcOpt: WithMaxQueryStackSize(1), + verify: func(t *testing.T, k Keeper) { + assert.IsType(t, uint32(1), k.maxQueryStackSize) + }, + }, + "accepted account types": { + srcOpt: WithAcceptedAccountTypesOnContractInstantiation(&authtypes.BaseAccount{}, &vestingtypes.ContinuousVestingAccount{}), + verify: func(t *testing.T, k Keeper) { + exp := map[reflect.Type]struct{}{ + reflect.TypeOf(&authtypes.BaseAccount{}): {}, + reflect.TypeOf(&vestingtypes.ContinuousVestingAccount{}): {}, + } + assert.Equal(t, exp, k.acceptedAccountTypes) + }, + }, + "account pruner": { + srcOpt: WithAccountPruner(VestingCoinBurner{}), + verify: func(t *testing.T, k Keeper) { + assert.Equal(t, VestingCoinBurner{}, k.accountPruner) + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + k := NewKeeper(nil, nil, paramtypes.NewSubspace(nil, nil, nil, nil, ""), authkeeper.AccountKeeper{}, &bankkeeper.BaseKeeper{}, stakingkeeper.Keeper{}, distributionkeeper.Keeper{}, nil, nil, nil, nil, nil, nil, "tempDir", types.DefaultWasmConfig(), AvailableCapabilities, spec.srcOpt) + spec.verify(t, k) + }) + } +} + +func setApiDefaults() { + costHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier + costCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier +} diff --git a/x/wasm/keeper/proposal_handler.go b/x/wasm/keeper/proposal_handler.go new file mode 100644 index 00000000..3f67a83a --- /dev/null +++ b/x/wasm/keeper/proposal_handler.go @@ -0,0 +1,326 @@ +package keeper + +import ( + "bytes" + "encoding/hex" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// NewWasmProposalHandler creates a new governance Handler for wasm proposals +func NewWasmProposalHandler(k decoratedKeeper, enabledProposalTypes []types.ProposalType) govtypes.Handler { + return NewWasmProposalHandlerX(NewGovPermissionKeeper(k), enabledProposalTypes) +} + +// NewWasmProposalHandlerX creates a new governance Handler for wasm proposals +func NewWasmProposalHandlerX(k types.ContractOpsKeeper, enabledProposalTypes []types.ProposalType) govtypes.Handler { + enabledTypes := make(map[string]struct{}, len(enabledProposalTypes)) + for i := range enabledProposalTypes { + enabledTypes[string(enabledProposalTypes[i])] = struct{}{} + } + return func(ctx sdk.Context, content govtypes.Content) error { + if content == nil { + return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "content must not be empty") + } + if _, ok := enabledTypes[content.ProposalType()]; !ok { + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unsupported wasm proposal content type: %q", content.ProposalType()) + } + switch c := content.(type) { + case *types.StoreCodeProposal: + return handleStoreCodeProposal(ctx, k, *c) + case *types.InstantiateContractProposal: + return handleInstantiateProposal(ctx, k, *c) + case *types.InstantiateContract2Proposal: + return handleInstantiate2Proposal(ctx, k, *c) + case *types.MigrateContractProposal: + return handleMigrateProposal(ctx, k, *c) + case *types.SudoContractProposal: + return handleSudoProposal(ctx, k, *c) + case *types.ExecuteContractProposal: + return handleExecuteProposal(ctx, k, *c) + case *types.UpdateAdminProposal: + return handleUpdateAdminProposal(ctx, k, *c) + case *types.ClearAdminProposal: + return handleClearAdminProposal(ctx, k, *c) + case *types.PinCodesProposal: + return handlePinCodesProposal(ctx, k, *c) + case *types.UnpinCodesProposal: + return handleUnpinCodesProposal(ctx, k, *c) + case *types.UpdateInstantiateConfigProposal: + return handleUpdateInstantiateConfigProposal(ctx, k, *c) + case *types.StoreAndInstantiateContractProposal: + return handleStoreAndInstantiateContractProposal(ctx, k, *c) + default: + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized wasm proposal content type: %T", c) + } + } +} + +func handleStoreCodeProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.StoreCodeProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs) + if err != nil { + return sdkerrors.Wrap(err, "run as address") + } + codeID, checksum, err := k.Create(ctx, runAsAddr, p.WASMByteCode, p.InstantiatePermission) + if err != nil { + return err + } + + if len(p.CodeHash) != 0 && !bytes.Equal(checksum, p.CodeHash) { + return fmt.Errorf("code-hash mismatch: %X, checksum: %X", p.CodeHash, checksum) + } + + // if code should not be pinned return earlier + if p.UnpinCode { + return nil + } + return k.PinCode(ctx, codeID) +} + +func handleInstantiateProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.InstantiateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs) + if err != nil { + return sdkerrors.Wrap(err, "run as address") + } + var adminAddr sdk.AccAddress + if p.Admin != "" { + if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + + _, data, err := k.Instantiate(ctx, p.CodeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeGovContractResult, + sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)), + )) + return nil +} + +func handleInstantiate2Proposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.InstantiateContract2Proposal) error { + // Validatebasic with proposal + if err := p.ValidateBasic(); err != nil { + return err + } + + // Get runAsAddr as AccAddress + runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs) + if err != nil { + return sdkerrors.Wrap(err, "run as address") + } + + // Get admin address + var adminAddr sdk.AccAddress + if p.Admin != "" { + if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + + _, data, err := k.Instantiate2(ctx, p.CodeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds, p.Salt, p.FixMsg) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeGovContractResult, + sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)), + )) + return nil +} + +func handleStoreAndInstantiateContractProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.StoreAndInstantiateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs) + if err != nil { + return sdkerrors.Wrap(err, "run as address") + } + var adminAddr sdk.AccAddress + if p.Admin != "" { + if adminAddr, err = sdk.AccAddressFromBech32(p.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + + codeID, checksum, err := k.Create(ctx, runAsAddr, p.WASMByteCode, p.InstantiatePermission) + if err != nil { + return err + } + + if p.CodeHash != nil && !bytes.Equal(checksum, p.CodeHash) { + return sdkerrors.Wrap(fmt.Errorf("code-hash mismatch: %X, checksum: %X", p.CodeHash, checksum), "code-hash mismatch") + } + + if !p.UnpinCode { + if err := k.PinCode(ctx, codeID); err != nil { + return err + } + } + + _, data, err := k.Instantiate(ctx, codeID, runAsAddr, adminAddr, p.Msg, p.Label, p.Funds) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeGovContractResult, + sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)), + )) + return nil +} + +func handleMigrateProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.MigrateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + contractAddr, err := sdk.AccAddressFromBech32(p.Contract) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + + // runAs is not used if this is permissioned, so just put any valid address there (second contractAddr) + data, err := k.Migrate(ctx, contractAddr, contractAddr, p.CodeID, p.Msg) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeGovContractResult, + sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)), + )) + return nil +} + +func handleSudoProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.SudoContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + contractAddr, err := sdk.AccAddressFromBech32(p.Contract) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + data, err := k.Sudo(ctx, contractAddr, p.Msg) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeGovContractResult, + sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)), + )) + return nil +} + +func handleExecuteProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.ExecuteContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + contractAddr, err := sdk.AccAddressFromBech32(p.Contract) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + runAsAddr, err := sdk.AccAddressFromBech32(p.RunAs) + if err != nil { + return sdkerrors.Wrap(err, "run as address") + } + data, err := k.Execute(ctx, contractAddr, runAsAddr, p.Msg, p.Funds) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent(sdk.NewEvent( + types.EventTypeGovContractResult, + sdk.NewAttribute(types.AttributeKeyResultDataHex, hex.EncodeToString(data)), + )) + return nil +} + +func handleUpdateAdminProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UpdateAdminProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + contractAddr, err := sdk.AccAddressFromBech32(p.Contract) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + newAdminAddr, err := sdk.AccAddressFromBech32(p.NewAdmin) + if err != nil { + return sdkerrors.Wrap(err, "run as address") + } + + return k.UpdateContractAdmin(ctx, contractAddr, nil, newAdminAddr) +} + +func handleClearAdminProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.ClearAdminProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + contractAddr, err := sdk.AccAddressFromBech32(p.Contract) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := k.ClearContractAdmin(ctx, contractAddr, nil); err != nil { + return err + } + return nil +} + +func handlePinCodesProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.PinCodesProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + for _, v := range p.CodeIDs { + if err := k.PinCode(ctx, v); err != nil { + return sdkerrors.Wrapf(err, "code id: %d", v) + } + } + return nil +} + +func handleUnpinCodesProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UnpinCodesProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + for _, v := range p.CodeIDs { + if err := k.UnpinCode(ctx, v); err != nil { + return sdkerrors.Wrapf(err, "code id: %d", v) + } + } + return nil +} + +func handleUpdateInstantiateConfigProposal(ctx sdk.Context, k types.ContractOpsKeeper, p types.UpdateInstantiateConfigProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + var emptyCaller sdk.AccAddress + for _, accessConfigUpdate := range p.AccessConfigUpdates { + if err := k.SetAccessConfig(ctx, accessConfigUpdate.CodeID, emptyCaller, accessConfigUpdate.InstantiatePermission); err != nil { + return sdkerrors.Wrapf(err, "code id: %d", accessConfigUpdate.CodeID) + } + } + return nil +} diff --git a/x/wasm/keeper/proposal_integration_test.go b/x/wasm/keeper/proposal_integration_test.go new file mode 100644 index 00000000..445d3a5d --- /dev/null +++ b/x/wasm/keeper/proposal_integration_test.go @@ -0,0 +1,1001 @@ +package keeper + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "os" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/params/client/utils" + "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestStoreCodeProposal(t *testing.T) { + parentCtx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(parentCtx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + checksum, err := hex.DecodeString("beb3de5e9b93b52e514c74ce87ccddb594b9bcd33b7f1af1bb6da63fc883917b") + require.NoError(t, err) + + specs := map[string]struct { + codeID int64 + unpinCode bool + }{ + "upload with pinning (default)": { + unpinCode: false, + }, + "upload with code unpin": { + unpinCode: true, + }, + } + + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + myActorAddress := RandomBech32AccountAddress(t) + + src := types.StoreCodeProposalFixture(func(p *types.StoreCodeProposal) { + p.RunAs = myActorAddress + p.WASMByteCode = wasmCode + p.UnpinCode = spec.unpinCode + p.CodeHash = checksum + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.GetContent()) + require.NoError(t, err) + + // then + cInfo := wasmKeeper.GetCodeInfo(ctx, 1) + require.NotNil(t, cInfo) + assert.Equal(t, myActorAddress, cInfo.Creator) + assert.Equal(t, !spec.unpinCode, wasmKeeper.IsPinnedCode(ctx, 1)) + + storedCode, err := wasmKeeper.GetByteCode(ctx, 1) + require.NoError(t, err) + assert.Equal(t, wasmCode, storedCode) + }) + } +} + +func TestInstantiateProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + require.NoError(t, wasmKeeper.importCode(ctx, 1, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + + var ( + oneAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, types.ContractAddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, types.ContractAddrLen) + ) + src := types.InstantiateContractProposalFixture(func(p *types.InstantiateContractProposal) { + p.CodeID = firstCodeID + p.RunAs = oneAddress.String() + p.Admin = otherAddress.String() + p.Label = "testing" + }) + em := sdk.NewEventManager() + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + // then + contractAddr, err := sdk.AccAddressFromBech32("cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr") + require.NoError(t, err) + + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(1), cInfo.CodeID) + assert.Equal(t, oneAddress.String(), cInfo.Creator) + assert.Equal(t, otherAddress.String(), cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: src.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.Msg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddr)) + // and event + require.Len(t, em.Events(), 3, "%#v", em.Events()) + require.Equal(t, types.EventTypeInstantiate, em.Events()[0].Type) + require.Equal(t, types.WasmModuleEventType, em.Events()[1].Type) + require.Equal(t, types.EventTypeGovContractResult, em.Events()[2].Type) + require.Len(t, em.Events()[2].Attributes, 1) + require.NotEmpty(t, em.Events()[2].Attributes[0]) +} + +func TestInstantiate2Proposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeInfo := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + err = wasmKeeper.importCode(ctx, 1, codeInfo, wasmCode) + require.NoError(t, err) + + var ( + oneAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, types.ContractAddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, types.ContractAddrLen) + label string = "label" + salt []byte = []byte("mySalt") + ) + src := types.InstantiateContract2ProposalFixture(func(p *types.InstantiateContract2Proposal) { + p.CodeID = firstCodeID + p.RunAs = oneAddress.String() + p.Admin = otherAddress.String() + p.Label = label + p.Salt = salt + }) + contractAddress := BuildContractAddressPredictable(codeInfo.CodeHash, oneAddress, salt, []byte{}) + + em := sdk.NewEventManager() + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddress) + require.NotNil(t, cInfo) + + assert.Equal(t, uint64(1), cInfo.CodeID) + assert.Equal(t, oneAddress.String(), cInfo.Creator) + assert.Equal(t, otherAddress.String(), cInfo.Admin) + assert.Equal(t, "label", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: src.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.Msg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddress)) + // and event + require.Len(t, em.Events(), 3, "%#v", em.Events()) + require.Equal(t, types.EventTypeInstantiate, em.Events()[0].Type) + require.Equal(t, types.WasmModuleEventType, em.Events()[1].Type) + require.Equal(t, types.EventTypeGovContractResult, em.Events()[2].Type) + require.Len(t, em.Events()[2].Attributes, 1) + require.NotEmpty(t, em.Events()[2].Attributes[0]) +} + +func TestInstantiateProposal_NoAdmin(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + require.NoError(t, wasmKeeper.importCode(ctx, 1, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + + var oneAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, types.ContractAddrLen) + + // test invalid admin address + src := types.InstantiateContractProposalFixture(func(p *types.InstantiateContractProposal) { + p.CodeID = firstCodeID + p.RunAs = oneAddress.String() + p.Admin = "invalid" + p.Label = "testing" + }) + _, err = govKeeper.SubmitProposal(ctx, src) + require.Error(t, err) + + // test with no admin + src = types.InstantiateContractProposalFixture(func(p *types.InstantiateContractProposal) { + p.CodeID = firstCodeID + p.RunAs = oneAddress.String() + p.Admin = "" + p.Label = "testing" + }) + em := sdk.NewEventManager() + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + // then + contractAddr, err := sdk.AccAddressFromBech32("cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr") + require.NoError(t, err) + + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(1), cInfo.CodeID) + assert.Equal(t, oneAddress.String(), cInfo.Creator) + assert.Equal(t, "", cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: src.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.Msg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddr)) + // and event + require.Len(t, em.Events(), 3, "%#v", em.Events()) + require.Equal(t, types.EventTypeInstantiate, em.Events()[0].Type) + require.Equal(t, types.WasmModuleEventType, em.Events()[1].Type) + require.Equal(t, types.EventTypeGovContractResult, em.Events()[2].Type) + require.Len(t, em.Events()[2].Attributes, 1) + require.NotEmpty(t, em.Events()[2].Attributes[0]) +} + +func TestStoreAndInstantiateContractProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + checksum, err := hex.DecodeString("beb3de5e9b93b52e514c74ce87ccddb594b9bcd33b7f1af1bb6da63fc883917b") + require.NoError(t, err) + + var ( + oneAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, types.ContractAddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, types.ContractAddrLen) + ) + + src := types.StoreAndInstantiateContractProposalFixture(func(p *types.StoreAndInstantiateContractProposal) { + p.WASMByteCode = wasmCode + p.RunAs = oneAddress.String() + p.Admin = otherAddress.String() + p.Label = "testing" + p.CodeHash = checksum + }) + em := sdk.NewEventManager() + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + // then + contractAddr, err := sdk.AccAddressFromBech32("cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr") + require.NoError(t, err) + + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, oneAddress.String(), cInfo.Creator) + assert.Equal(t, otherAddress.String(), cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: cInfo.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.Msg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddr)) + // and event + require.Len(t, em.Events(), 5, "%#v", em.Events()) + require.Equal(t, types.EventTypeStoreCode, em.Events()[0].Type) + require.Equal(t, types.EventTypePinCode, em.Events()[1].Type) + require.Equal(t, types.EventTypeInstantiate, em.Events()[2].Type) + require.Equal(t, types.WasmModuleEventType, em.Events()[3].Type) + require.Equal(t, types.EventTypeGovContractResult, em.Events()[4].Type) + require.Len(t, em.Events()[4].Attributes, 1) + require.NotEmpty(t, em.Events()[4].Attributes[0]) +} + +func TestMigrateProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeInfoFixture := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfoFixture, wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 2, codeInfoFixture, wasmCode)) + + var ( + anyAddress = DeterministicAccountAddress(t, 1) + otherAddress = DeterministicAccountAddress(t, 2) + contractAddr = BuildContractAddressClassic(1, 1) + ) + + contractInfo := types.ContractInfoFixture(func(c *types.ContractInfo) { + c.Label = "testing" + c.Admin = anyAddress.String() + c.Created = types.NewAbsoluteTxPosition(ctx) + }) + entries := []types.ContractCodeHistoryEntry{ + {Operation: types.ContractCodeHistoryOperationTypeInit, CodeID: 1, Updated: contractInfo.Created}, + } + key, err := hex.DecodeString("636F6E666967") + require.NoError(t, err) + m := types.Model{Key: key, Value: []byte(`{"verifier":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","beneficiary":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","funder":"AQEBAQEBAQEBAQEBAQEBAQEBAQE="}`)} + require.NoError(t, wasmKeeper.importContract(ctx, contractAddr, &contractInfo, []types.Model{m}, entries)) + + migMsg := struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: otherAddress} + migMsgBz, err := json.Marshal(migMsg) + require.NoError(t, err) + + src := types.MigrateContractProposal{ + Title: "Foo", + Description: "Bar", + CodeID: 2, + Contract: contractAddr.String(), + Msg: migMsgBz, + } + + em := sdk.NewEventManager() + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, &src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + // then + require.NoError(t, err) + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(2), cInfo.CodeID) + assert.Equal(t, anyAddress.String(), cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: src.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.Msg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddr)) + // and events emitted + require.Len(t, em.Events(), 2) + assert.Equal(t, types.EventTypeMigrate, em.Events()[0].Type) + require.Equal(t, types.EventTypeGovContractResult, em.Events()[1].Type) + require.Len(t, em.Events()[1].Attributes, 1) + assert.Equal(t, types.AttributeKeyResultDataHex, string(em.Events()[1].Attributes[0].Key)) +} + +func TestExecuteProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, bankKeeper := keepers.GovKeeper, keepers.BankKeeper + + exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) + contractAddr := exampleContract.Contract + + // check balance + bal := bankKeeper.GetBalance(ctx, contractAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(100)) + + releaseMsg := struct { + Release struct{} `json:"release"` + }{} + releaseMsgBz, err := json.Marshal(releaseMsg) + require.NoError(t, err) + + // try with runAs that doesn't have pemission + badSrc := types.ExecuteContractProposal{ + Title: "First", + Description: "Beneficiary has no permission to run", + Contract: contractAddr.String(), + Msg: releaseMsgBz, + RunAs: exampleContract.BeneficiaryAddr.String(), + } + + em := sdk.NewEventManager() + + // fails on store - this doesn't have permission + storedProposal, err := govKeeper.SubmitProposal(ctx, &badSrc) + require.Error(t, err) + // balance should not change + bal = bankKeeper.GetBalance(ctx, contractAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(100)) + + // try again with the proper run-as + src := types.ExecuteContractProposal{ + Title: "Second", + Description: "Verifier can execute", + Contract: contractAddr.String(), + Msg: releaseMsgBz, + RunAs: exampleContract.VerifierAddr.String(), + } + + em = sdk.NewEventManager() + + // when stored + storedProposal, err = govKeeper.SubmitProposal(ctx, &src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + // balance should be empty (proper release) + bal = bankKeeper.GetBalance(ctx, contractAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(0)) +} + +func TestSudoProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, bankKeeper := keepers.GovKeeper, keepers.BankKeeper + + exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) + contractAddr := exampleContract.Contract + _, _, anyAddr := keyPubAddr() + + // check balance + bal := bankKeeper.GetBalance(ctx, contractAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(100)) + bal = bankKeeper.GetBalance(ctx, anyAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(0)) + + type StealMsg struct { + Recipient string `json:"recipient"` + Amount []sdk.Coin `json:"amount"` + } + stealMsg := struct { + Steal StealMsg `json:"steal_funds"` + }{Steal: StealMsg{ + Recipient: anyAddr.String(), + Amount: []sdk.Coin{sdk.NewInt64Coin("denom", 75)}, + }} + stealMsgBz, err := json.Marshal(stealMsg) + require.NoError(t, err) + + // sudo can do anything + src := types.SudoContractProposal{ + Title: "Sudo", + Description: "Steal funds for the verifier", + Contract: contractAddr.String(), + Msg: stealMsgBz, + } + + em := sdk.NewEventManager() + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, &src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx.WithEventManager(em), storedProposal.GetContent()) + require.NoError(t, err) + + // balance should be empty (and verifier richer) + bal = bankKeeper.GetBalance(ctx, contractAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(25)) + bal = bankKeeper.GetBalance(ctx, anyAddr, "denom") + require.Equal(t, bal.Amount, sdk.NewInt(75)) +} + +func TestAdminProposals(t *testing.T) { + var ( + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, types.ContractAddrLen) + contractAddr = BuildContractAddressClassic(1, 1) + ) + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + specs := map[string]struct { + state types.ContractInfo + srcProposal govtypes.Content + expAdmin sdk.AccAddress + }{ + "update with different admin": { + state: types.ContractInfoFixture(), + srcProposal: &types.UpdateAdminProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr.String(), + NewAdmin: otherAddress.String(), + }, + expAdmin: otherAddress, + }, + "update with old admin empty": { + state: types.ContractInfoFixture(func(info *types.ContractInfo) { + info.Admin = "" + }), + srcProposal: &types.UpdateAdminProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr.String(), + NewAdmin: otherAddress.String(), + }, + expAdmin: otherAddress, + }, + "clear admin": { + state: types.ContractInfoFixture(), + srcProposal: &types.ClearAdminProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr.String(), + }, + expAdmin: nil, + }, + "clear with old admin empty": { + state: types.ContractInfoFixture(func(info *types.ContractInfo) { + info.Admin = "" + }), + srcProposal: &types.ClearAdminProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr.String(), + }, + expAdmin: nil, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + codeInfo := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfo, wasmCode)) + + entries := []types.ContractCodeHistoryEntry{ + { + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: 1, + Updated: spec.state.Created, + }, + } + + require.NoError(t, wasmKeeper.importContract(ctx, contractAddr, &spec.state, []types.Model{}, entries)) + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, spec.srcProposal) + require.NoError(t, err) + + // and execute proposal + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.GetContent()) + require.NoError(t, err) + + // then + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, spec.expAdmin.String(), cInfo.Admin) + }) + } +} + +func TestUpdateParamsProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + var ( + legacyAmino = keepers.EncodingConfig.Amino + myAddress sdk.AccAddress = make([]byte, types.ContractAddrLen) + oneAddressAccessConfig = types.AccessTypeOnlyAddress.With(myAddress) + ) + + specs := map[string]struct { + src proposal.ParamChange + expUploadConfig types.AccessConfig + expInstantiateType types.AccessType + }{ + "update upload permission param": { + src: proposal.ParamChange{ + Subspace: types.ModuleName, + Key: string(types.ParamStoreKeyUploadAccess), + Value: string(legacyAmino.MustMarshalJSON(&types.AllowNobody)), + }, + expUploadConfig: types.AllowNobody, + expInstantiateType: types.AccessTypeEverybody, + }, + "update upload permission with same as current value": { + src: proposal.ParamChange{ + Subspace: types.ModuleName, + Key: string(types.ParamStoreKeyUploadAccess), + Value: string(legacyAmino.MustMarshalJSON(&types.AllowEverybody)), + }, + expUploadConfig: types.AllowEverybody, + expInstantiateType: types.AccessTypeEverybody, + }, + "update upload permission param with address": { + src: proposal.ParamChange{ + Subspace: types.ModuleName, + Key: string(types.ParamStoreKeyUploadAccess), + Value: string(legacyAmino.MustMarshalJSON(&oneAddressAccessConfig)), + }, + expUploadConfig: oneAddressAccessConfig, + expInstantiateType: types.AccessTypeEverybody, + }, + "update instantiate param": { + src: proposal.ParamChange{ + Subspace: types.ModuleName, + Key: string(types.ParamStoreKeyInstantiateAccess), + Value: string(legacyAmino.MustMarshalJSON(types.AccessTypeNobody)), + }, + expUploadConfig: types.AllowEverybody, + expInstantiateType: types.AccessTypeNobody, + }, + "update instantiate param as default": { + src: proposal.ParamChange{ + Subspace: types.ModuleName, + Key: string(types.ParamStoreKeyInstantiateAccess), + Value: string(legacyAmino.MustMarshalJSON(types.AccessTypeEverybody)), + }, + expUploadConfig: types.AllowEverybody, + expInstantiateType: types.AccessTypeEverybody, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + wasmKeeper.SetParams(ctx, types.DefaultParams()) + + // encode + decode as CLI to play nice with amino + bz := legacyAmino.MustMarshalJSON(&utils.ParamChangeProposalJSON{ + Title: "Foo", + Description: "Bar", + Changes: []utils.ParamChangeJSON{{Subspace: spec.src.Subspace, Key: spec.src.Key, Value: json.RawMessage(spec.src.Value)}}, + }) + t.Log(string(bz)) + + var jsonProposal utils.ParamChangeProposalJSON + require.NoError(t, legacyAmino.UnmarshalJSON(bz, &jsonProposal)) + proposal := proposal.ParameterChangeProposal{ + Title: jsonProposal.Title, + Description: jsonProposal.Description, + Changes: jsonProposal.Changes.ToParamChanges(), + } + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, &proposal) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.GetContent()) + require.NoError(t, err) + + // then + assert.True(t, spec.expUploadConfig.Equals(wasmKeeper.getUploadAccessConfig(ctx)), + "got %#v not %#v", wasmKeeper.getUploadAccessConfig(ctx), spec.expUploadConfig) + assert.Equal(t, spec.expInstantiateType, wasmKeeper.getInstantiateAccessConfig(ctx)) + }) + } +} + +func TestPinCodesProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + mock := wasmtesting.MockWasmer{ + CreateFn: wasmtesting.NoOpCreateFn, + AnalyzeCodeFn: wasmtesting.WithoutIBCAnalyzeFn, + } + var ( + hackatom = StoreHackatomExampleContract(t, ctx, keepers) + hackatomDuplicate = StoreHackatomExampleContract(t, ctx, keepers) + otherContract = StoreRandomContract(t, ctx, keepers, &mock) + gotPinnedChecksums []wasmvm.Checksum + ) + checksumCollector := func(checksum wasmvm.Checksum) error { + gotPinnedChecksums = append(gotPinnedChecksums, checksum) + return nil + } + specs := map[string]struct { + srcCodeIDs []uint64 + mockFn func(checksum wasmvm.Checksum) error + expPinned []wasmvm.Checksum + expErr bool + }{ + "pin one": { + srcCodeIDs: []uint64{hackatom.CodeID}, + mockFn: checksumCollector, + }, + "pin multiple": { + srcCodeIDs: []uint64{hackatom.CodeID, otherContract.CodeID}, + mockFn: checksumCollector, + }, + "pin same code id": { + srcCodeIDs: []uint64{hackatom.CodeID, hackatomDuplicate.CodeID}, + mockFn: checksumCollector, + }, + "pin non existing code id": { + srcCodeIDs: []uint64{999}, + mockFn: checksumCollector, + expErr: true, + }, + "pin empty code id list": { + srcCodeIDs: []uint64{}, + mockFn: checksumCollector, + expErr: true, + }, + "wasmvm failed with error": { + srcCodeIDs: []uint64{hackatom.CodeID}, + mockFn: func(_ wasmvm.Checksum) error { + return errors.New("test, ignore") + }, + expErr: true, + }, + } + parentCtx := ctx + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + gotPinnedChecksums = nil + ctx, _ := parentCtx.CacheContext() + mock.PinFn = spec.mockFn + proposal := types.PinCodesProposal{ + Title: "Foo", + Description: "Bar", + CodeIDs: spec.srcCodeIDs, + } + + // when stored + storedProposal, gotErr := govKeeper.SubmitProposal(ctx, &proposal) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + gotErr = handler(ctx, storedProposal.GetContent()) + require.NoError(t, gotErr) + + // then + for i := range spec.srcCodeIDs { + c := wasmKeeper.GetCodeInfo(ctx, spec.srcCodeIDs[i]) + require.Equal(t, wasmvm.Checksum(c.CodeHash), gotPinnedChecksums[i]) + } + }) + } +} + +func TestUnpinCodesProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + mock := wasmtesting.MockWasmer{ + CreateFn: wasmtesting.NoOpCreateFn, + AnalyzeCodeFn: wasmtesting.WithoutIBCAnalyzeFn, + } + var ( + hackatom = StoreHackatomExampleContract(t, ctx, keepers) + hackatomDuplicate = StoreHackatomExampleContract(t, ctx, keepers) + otherContract = StoreRandomContract(t, ctx, keepers, &mock) + gotUnpinnedChecksums []wasmvm.Checksum + ) + checksumCollector := func(checksum wasmvm.Checksum) error { + gotUnpinnedChecksums = append(gotUnpinnedChecksums, checksum) + return nil + } + specs := map[string]struct { + srcCodeIDs []uint64 + mockFn func(checksum wasmvm.Checksum) error + expUnpinned []wasmvm.Checksum + expErr bool + }{ + "unpin one": { + srcCodeIDs: []uint64{hackatom.CodeID}, + mockFn: checksumCollector, + }, + "unpin multiple": { + srcCodeIDs: []uint64{hackatom.CodeID, otherContract.CodeID}, + mockFn: checksumCollector, + }, + "unpin same code id": { + srcCodeIDs: []uint64{hackatom.CodeID, hackatomDuplicate.CodeID}, + mockFn: checksumCollector, + }, + "unpin non existing code id": { + srcCodeIDs: []uint64{999}, + mockFn: checksumCollector, + expErr: true, + }, + "unpin empty code id list": { + srcCodeIDs: []uint64{}, + mockFn: checksumCollector, + expErr: true, + }, + "wasmvm failed with error": { + srcCodeIDs: []uint64{hackatom.CodeID}, + mockFn: func(_ wasmvm.Checksum) error { + return errors.New("test, ignore") + }, + expErr: true, + }, + } + parentCtx := ctx + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + gotUnpinnedChecksums = nil + ctx, _ := parentCtx.CacheContext() + mock.UnpinFn = spec.mockFn + proposal := types.UnpinCodesProposal{ + Title: "Foo", + Description: "Bar", + CodeIDs: spec.srcCodeIDs, + } + + // when stored + storedProposal, gotErr := govKeeper.SubmitProposal(ctx, &proposal) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + gotErr = handler(ctx, storedProposal.GetContent()) + require.NoError(t, gotErr) + + // then + for i := range spec.srcCodeIDs { + c := wasmKeeper.GetCodeInfo(ctx, spec.srcCodeIDs[i]) + require.Equal(t, wasmvm.Checksum(c.CodeHash), gotUnpinnedChecksums[i]) + } + }) + } +} + +func TestUpdateInstantiateConfigProposal(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, "staking") + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + mock := wasmtesting.MockWasmer{ + CreateFn: wasmtesting.NoOpCreateFn, + AnalyzeCodeFn: wasmtesting.WithoutIBCAnalyzeFn, + } + anyAddress, err := sdk.AccAddressFromBech32("cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz") + require.NoError(t, err) + + withAddressAccessConfig := types.AccessTypeAnyOfAddresses.With(anyAddress) + var ( + nobody = StoreRandomContractWithAccessConfig(t, ctx, keepers, &mock, &types.AllowNobody) + everybody = StoreRandomContractWithAccessConfig(t, ctx, keepers, &mock, &types.AllowEverybody) + withAddress = StoreRandomContractWithAccessConfig(t, ctx, keepers, &mock, &withAddressAccessConfig) + ) + + specs := map[string]struct { + accessConfigUpdates []types.AccessConfigUpdate + expErr bool + }{ + "update one": { + accessConfigUpdates: []types.AccessConfigUpdate{ + {CodeID: nobody.CodeID, InstantiatePermission: types.AllowEverybody}, + }, + }, + "update multiple": { + accessConfigUpdates: []types.AccessConfigUpdate{ + {CodeID: everybody.CodeID, InstantiatePermission: types.AllowNobody}, + {CodeID: nobody.CodeID, InstantiatePermission: withAddressAccessConfig}, + {CodeID: withAddress.CodeID, InstantiatePermission: types.AllowEverybody}, + }, + }, + "update same code id": { + accessConfigUpdates: []types.AccessConfigUpdate{ + {CodeID: everybody.CodeID, InstantiatePermission: types.AllowNobody}, + {CodeID: everybody.CodeID, InstantiatePermission: types.AllowEverybody}, + }, + expErr: true, + }, + "update non existing code id": { + accessConfigUpdates: []types.AccessConfigUpdate{ + {CodeID: 100, InstantiatePermission: types.AllowNobody}, + {CodeID: everybody.CodeID, InstantiatePermission: types.AllowEverybody}, + }, + expErr: true, + }, + "update empty list": { + accessConfigUpdates: make([]types.AccessConfigUpdate, 0), + expErr: true, + }, + } + parentCtx := ctx + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx, _ := parentCtx.CacheContext() + + updates := make([]types.AccessConfigUpdate, 0) + for _, cu := range spec.accessConfigUpdates { + updates = append(updates, types.AccessConfigUpdate{ + CodeID: cu.CodeID, + InstantiatePermission: cu.InstantiatePermission, + }) + } + + proposal := types.UpdateInstantiateConfigProposal{ + Title: "Foo", + Description: "Bar", + AccessConfigUpdates: updates, + } + + // when stored + storedProposal, gotErr := govKeeper.SubmitProposal(ctx, &proposal) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + gotErr = handler(ctx, storedProposal.GetContent()) + require.NoError(t, gotErr) + + // then + for i := range spec.accessConfigUpdates { + c := wasmKeeper.GetCodeInfo(ctx, spec.accessConfigUpdates[i].CodeID) + require.Equal(t, spec.accessConfigUpdates[i].InstantiatePermission, c.InstantiateConfig) + } + }) + } +} diff --git a/x/wasm/keeper/querier.go b/x/wasm/keeper/querier.go new file mode 100644 index 00000000..eebfb66a --- /dev/null +++ b/x/wasm/keeper/querier.go @@ -0,0 +1,346 @@ +package keeper + +import ( + "context" + "encoding/binary" + "runtime/debug" + + "github.com/cosmos/cosmos-sdk/codec" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/query" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ types.QueryServer = &grpcQuerier{} + +type grpcQuerier struct { + cdc codec.Codec + storeKey sdk.StoreKey + keeper types.ViewKeeper + queryGasLimit sdk.Gas +} + +// NewGrpcQuerier constructor +func NewGrpcQuerier(cdc codec.Codec, storeKey sdk.StoreKey, keeper types.ViewKeeper, queryGasLimit sdk.Gas) *grpcQuerier { //nolint:revive + return &grpcQuerier{cdc: cdc, storeKey: storeKey, keeper: keeper, queryGasLimit: queryGasLimit} +} + +func (q grpcQuerier) ContractInfo(c context.Context, req *types.QueryContractInfoRequest) (*types.QueryContractInfoResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + contractAddr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, err + } + rsp, err := queryContractInfo(sdk.UnwrapSDKContext(c), contractAddr, q.keeper) + switch { + case err != nil: + return nil, err + case rsp == nil: + return nil, types.ErrNotFound + } + return rsp, nil +} + +func (q grpcQuerier) ContractHistory(c context.Context, req *types.QueryContractHistoryRequest) (*types.QueryContractHistoryResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + contractAddr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + r := make([]types.ContractCodeHistoryEntry, 0) + + prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractCodeHistoryElementPrefix(contractAddr)) + pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) { + if accumulate { + var e types.ContractCodeHistoryEntry + if err := q.cdc.Unmarshal(value, &e); err != nil { + return false, err + } + r = append(r, e) + } + return true, nil + }) + if err != nil { + return nil, err + } + return &types.QueryContractHistoryResponse{ + Entries: r, + Pagination: pageRes, + }, nil +} + +// ContractsByCode lists all smart contracts for a code id +func (q grpcQuerier) ContractsByCode(c context.Context, req *types.QueryContractsByCodeRequest) (*types.QueryContractsByCodeResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + if req.CodeId == 0 { + return nil, sdkerrors.Wrap(types.ErrInvalid, "code id") + } + ctx := sdk.UnwrapSDKContext(c) + r := make([]string, 0) + + prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractByCodeIDSecondaryIndexPrefix(req.CodeId)) + pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) { + if accumulate { + var contractAddr sdk.AccAddress = key[types.AbsoluteTxPositionLen:] + r = append(r, contractAddr.String()) + } + return true, nil + }) + if err != nil { + return nil, err + } + return &types.QueryContractsByCodeResponse{ + Contracts: r, + Pagination: pageRes, + }, nil +} + +func (q grpcQuerier) AllContractState(c context.Context, req *types.QueryAllContractStateRequest) (*types.QueryAllContractStateResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + contractAddr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, err + } + ctx := sdk.UnwrapSDKContext(c) + if !q.keeper.HasContractInfo(ctx, contractAddr) { + return nil, types.ErrNotFound + } + + r := make([]types.Model, 0) + prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractStorePrefix(contractAddr)) + pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) { + if accumulate { + r = append(r, types.Model{ + Key: key, + Value: value, + }) + } + return true, nil + }) + if err != nil { + return nil, err + } + return &types.QueryAllContractStateResponse{ + Models: r, + Pagination: pageRes, + }, nil +} + +func (q grpcQuerier) RawContractState(c context.Context, req *types.QueryRawContractStateRequest) (*types.QueryRawContractStateResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + ctx := sdk.UnwrapSDKContext(c) + + contractAddr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, err + } + + if !q.keeper.HasContractInfo(ctx, contractAddr) { + return nil, types.ErrNotFound + } + rsp := q.keeper.QueryRaw(ctx, contractAddr, req.QueryData) + return &types.QueryRawContractStateResponse{Data: rsp}, nil +} + +func (q grpcQuerier) SmartContractState(c context.Context, req *types.QuerySmartContractStateRequest) (rsp *types.QuerySmartContractStateResponse, err error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + if err := req.QueryData.ValidateBasic(); err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid query data") + } + contractAddr, err := sdk.AccAddressFromBech32(req.Address) + if err != nil { + return nil, err + } + ctx := sdk.UnwrapSDKContext(c).WithGasMeter(sdk.NewGasMeter(q.queryGasLimit)) + // recover from out-of-gas panic + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + err = sdkerrors.Wrapf(sdkerrors.ErrOutOfGas, + "out of gas in location: %v; gasWanted: %d, gasUsed: %d", + rType.Descriptor, ctx.GasMeter().Limit(), ctx.GasMeter().GasConsumed(), + ) + default: + err = sdkerrors.ErrPanic + } + rsp = nil + moduleLogger(ctx). + Debug("smart query contract", + "error", "recovering panic", + "contract-address", req.Address, + "stacktrace", string(debug.Stack())) + } + }() + + bz, err := q.keeper.QuerySmart(ctx, contractAddr, req.QueryData) + switch { + case err != nil: + return nil, err + case bz == nil: + return nil, types.ErrNotFound + } + return &types.QuerySmartContractStateResponse{Data: bz}, nil +} + +func (q grpcQuerier) Code(c context.Context, req *types.QueryCodeRequest) (*types.QueryCodeResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + if req.CodeId == 0 { + return nil, sdkerrors.Wrap(types.ErrInvalid, "code id") + } + rsp, err := queryCode(sdk.UnwrapSDKContext(c), req.CodeId, q.keeper) + switch { + case err != nil: + return nil, err + case rsp == nil: + return nil, types.ErrNotFound + } + return &types.QueryCodeResponse{ + CodeInfoResponse: rsp.CodeInfoResponse, + Data: rsp.Data, + }, nil +} + +func (q grpcQuerier) Codes(c context.Context, req *types.QueryCodesRequest) (*types.QueryCodesResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + ctx := sdk.UnwrapSDKContext(c) + r := make([]types.CodeInfoResponse, 0) + prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.CodeKeyPrefix) + pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) { + if accumulate { + var c types.CodeInfo + if err := q.cdc.Unmarshal(value, &c); err != nil { + return false, err + } + r = append(r, types.CodeInfoResponse{ + CodeID: binary.BigEndian.Uint64(key), + Creator: c.Creator, + DataHash: c.CodeHash, + InstantiatePermission: c.InstantiateConfig, + }) + } + return true, nil + }) + if err != nil { + return nil, err + } + return &types.QueryCodesResponse{CodeInfos: r, Pagination: pageRes}, nil +} + +func queryContractInfo(ctx sdk.Context, addr sdk.AccAddress, keeper types.ViewKeeper) (*types.QueryContractInfoResponse, error) { + info := keeper.GetContractInfo(ctx, addr) + if info == nil { + return nil, types.ErrNotFound + } + return &types.QueryContractInfoResponse{ + Address: addr.String(), + ContractInfo: *info, + }, nil +} + +func queryCode(ctx sdk.Context, codeID uint64, keeper types.ViewKeeper) (*types.QueryCodeResponse, error) { + if codeID == 0 { + return nil, nil + } + res := keeper.GetCodeInfo(ctx, codeID) + if res == nil { + // nil, nil leads to 404 in rest handler + return nil, nil + } + info := types.CodeInfoResponse{ + CodeID: codeID, + Creator: res.Creator, + DataHash: res.CodeHash, + InstantiatePermission: res.InstantiateConfig, + } + + code, err := keeper.GetByteCode(ctx, codeID) + if err != nil { + return nil, sdkerrors.Wrap(err, "loading wasm code") + } + + return &types.QueryCodeResponse{CodeInfoResponse: &info, Data: code}, nil +} + +func (q grpcQuerier) PinnedCodes(c context.Context, req *types.QueryPinnedCodesRequest) (*types.QueryPinnedCodesResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + ctx := sdk.UnwrapSDKContext(c) + r := make([]uint64, 0) + + prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.PinnedCodeIndexPrefix) + pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) { + if accumulate { + r = append(r, sdk.BigEndianToUint64(key)) + } + return true, nil + }) + if err != nil { + return nil, err + } + return &types.QueryPinnedCodesResponse{ + CodeIDs: r, + Pagination: pageRes, + }, nil +} + +// Params returns params of the module. +func (q grpcQuerier) Params(c context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + params := q.keeper.GetParams(ctx) + return &types.QueryParamsResponse{Params: params}, nil +} + +func (q grpcQuerier) ContractsByCreator(c context.Context, req *types.QueryContractsByCreatorRequest) (*types.QueryContractsByCreatorResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + ctx := sdk.UnwrapSDKContext(c) + contracts := make([]string, 0) + + creatorAddress, err := sdk.AccAddressFromBech32(req.CreatorAddress) + if err != nil { + return nil, err + } + prefixStore := prefix.NewStore(ctx.KVStore(q.storeKey), types.GetContractsByCreatorPrefix(creatorAddress)) + pageRes, err := query.FilteredPaginate(prefixStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) { + if accumulate { + accAddres := sdk.AccAddress(key[types.AbsoluteTxPositionLen:]) + contracts = append(contracts, accAddres.String()) + } + return true, nil + }) + if err != nil { + return nil, err + } + + return &types.QueryContractsByCreatorResponse{ + ContractAddresses: contracts, + Pagination: pageRes, + }, nil +} diff --git a/x/wasm/keeper/querier_test.go b/x/wasm/keeper/querier_test.go new file mode 100644 index 00000000..da25b35f --- /dev/null +++ b/x/wasm/keeper/querier_test.go @@ -0,0 +1,917 @@ +package keeper + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "testing" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkErrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/query" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestQueryAllContractState(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) + contractAddr := exampleContract.Contract + contractModel := []types.Model{ + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + } + require.NoError(t, keeper.importContractState(ctx, contractAddr, contractModel)) + + q := Querier(keeper) + specs := map[string]struct { + srcQuery *types.QueryAllContractStateRequest + expModelContains []types.Model + expModelContainsNot []types.Model + expErr *sdkErrors.Error + }{ + "query all": { + srcQuery: &types.QueryAllContractStateRequest{Address: contractAddr.String()}, + expModelContains: contractModel, + }, + "query all with unknown address": { + srcQuery: &types.QueryAllContractStateRequest{Address: RandomBech32AccountAddress(t)}, + expErr: types.ErrNotFound, + }, + "with pagination offset": { + srcQuery: &types.QueryAllContractStateRequest{ + Address: contractAddr.String(), + Pagination: &query.PageRequest{ + Offset: 1, + }, + }, + expModelContains: []types.Model{ + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + }, + expModelContainsNot: []types.Model{ + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + }, + }, + "with pagination limit": { + srcQuery: &types.QueryAllContractStateRequest{ + Address: contractAddr.String(), + Pagination: &query.PageRequest{ + Limit: 1, + }, + }, + expModelContains: []types.Model{ + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + }, + expModelContainsNot: []types.Model{ + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + }, + }, + "with pagination next key": { + srcQuery: &types.QueryAllContractStateRequest{ + Address: contractAddr.String(), + Pagination: &query.PageRequest{ + Key: fromBase64("Y29uZmln"), + }, + }, + expModelContains: []types.Model{ + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + }, + expModelContainsNot: []types.Model{ + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + }, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := q.AllContractState(sdk.WrapSDKContext(ctx), spec.srcQuery) + require.True(t, spec.expErr.Is(err), err) + if spec.expErr != nil { + return + } + for _, exp := range spec.expModelContains { + assert.Contains(t, got.Models, exp) + } + for _, exp := range spec.expModelContainsNot { + assert.NotContains(t, got.Models, exp) + } + }) + } +} + +func TestQuerySmartContractState(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) + contractAddr := exampleContract.Contract.String() + + q := Querier(keeper) + specs := map[string]struct { + srcAddr sdk.AccAddress + srcQuery *types.QuerySmartContractStateRequest + expResp string + expErr error + }{ + "query smart": { + srcQuery: &types.QuerySmartContractStateRequest{Address: contractAddr, QueryData: []byte(`{"verifier":{}}`)}, + expResp: fmt.Sprintf(`{"verifier":"%s"}`, exampleContract.VerifierAddr.String()), + }, + "query smart invalid request": { + srcQuery: &types.QuerySmartContractStateRequest{Address: contractAddr, QueryData: []byte(`{"raw":{"key":"config"}}`)}, + expErr: types.ErrQueryFailed, + }, + "query smart with invalid json": { + srcQuery: &types.QuerySmartContractStateRequest{Address: contractAddr, QueryData: []byte(`not a json string`)}, + expErr: status.Error(codes.InvalidArgument, "invalid query data"), + }, + "query smart with unknown address": { + srcQuery: &types.QuerySmartContractStateRequest{Address: RandomBech32AccountAddress(t), QueryData: []byte(`{"verifier":{}}`)}, + expErr: types.ErrNotFound, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := q.SmartContractState(sdk.WrapSDKContext(ctx), spec.srcQuery) + require.True(t, errors.Is(err, spec.expErr), "but got %+v", err) + if spec.expErr != nil { + return + } + assert.JSONEq(t, string(got.Data), spec.expResp) + }) + } +} + +func TestQuerySmartContractPanics(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + contractAddr := BuildContractAddressClassic(1, 1) + keepers.WasmKeeper.storeCodeInfo(ctx, 1, types.CodeInfo{}) + keepers.WasmKeeper.storeContractInfo(ctx, contractAddr, &types.ContractInfo{ + CodeID: 1, + Created: types.NewAbsoluteTxPosition(ctx), + }) + ctx = ctx.WithGasMeter(sdk.NewGasMeter(DefaultInstanceCost)).WithLogger(log.TestingLogger()) + + specs := map[string]struct { + doInContract func() + expErr *sdkErrors.Error + }{ + "out of gas": { + doInContract: func() { + ctx.GasMeter().ConsumeGas(ctx.GasMeter().Limit()+1, "test - consume more than limit") + }, + expErr: sdkErrors.ErrOutOfGas, + }, + "other panic": { + doInContract: func() { + panic("my panic") + }, + expErr: sdkErrors.ErrPanic, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + keepers.WasmKeeper.wasmVM = &wasmtesting.MockWasmer{QueryFn: func(checksum wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) { + spec.doInContract() + return nil, 0, nil + }} + // when + q := Querier(keepers.WasmKeeper) + got, err := q.SmartContractState(sdk.WrapSDKContext(ctx), &types.QuerySmartContractStateRequest{ + Address: contractAddr.String(), + QueryData: types.RawContractMessage("{}"), + }) + require.True(t, spec.expErr.Is(err), "got error: %+v", err) + assert.Nil(t, got) + }) + } +} + +func TestQueryRawContractState(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) + contractAddr := exampleContract.Contract.String() + contractModel := []types.Model{ + {Key: []byte("foo"), Value: []byte(`"bar"`)}, + {Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)}, + } + require.NoError(t, keeper.importContractState(ctx, exampleContract.Contract, contractModel)) + + q := Querier(keeper) + specs := map[string]struct { + srcQuery *types.QueryRawContractStateRequest + expData []byte + expErr *sdkErrors.Error + }{ + "query raw key": { + srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte("foo")}, + expData: []byte(`"bar"`), + }, + "query raw contract binary key": { + srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte{0x0, 0x1}}, + expData: []byte(`{"count":8}`), + }, + "query non-existent raw key": { + srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte("not existing key")}, + expData: nil, + }, + "query empty raw key": { + srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr, QueryData: []byte("")}, + expData: nil, + }, + "query nil raw key": { + srcQuery: &types.QueryRawContractStateRequest{Address: contractAddr}, + expData: nil, + }, + "query raw with unknown address": { + srcQuery: &types.QueryRawContractStateRequest{Address: RandomBech32AccountAddress(t), QueryData: []byte("foo")}, + expErr: types.ErrNotFound, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := q.RawContractState(sdk.WrapSDKContext(ctx), spec.srcQuery) + require.True(t, spec.expErr.Is(err), err) + if spec.expErr != nil { + return + } + assert.Equal(t, spec.expData, got.Data) + }) + } +} + +func TestQueryContractListByCodeOrdering(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: anyAddr, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + // manage some realistic block settings + var h int64 = 10 + setBlock := func(ctx sdk.Context, height int64) sdk.Context { + ctx = ctx.WithBlockHeight(height) + meter := sdk.NewGasMeter(1000000) + ctx = ctx.WithGasMeter(meter) + ctx = ctx.WithBlockGasMeter(meter) + return ctx + } + + // create 10 contracts with real block/gas setup + for i := 0; i < 10; i++ { + // 3 tx per block, so we ensure both comparisons work + if i%3 == 0 { + ctx = setBlock(ctx, h) + h++ + } + _, _, err = keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, fmt.Sprintf("contract %d", i), topUp) + require.NoError(t, err) + } + + // query and check the results are properly sorted + q := Querier(keeper) + res, err := q.ContractsByCode(sdk.WrapSDKContext(ctx), &types.QueryContractsByCodeRequest{CodeId: codeID}) + require.NoError(t, err) + + require.Equal(t, 10, len(res.Contracts)) + + for _, contractAddr := range res.Contracts { + assert.NotEmpty(t, contractAddr) + } +} + +func TestQueryContractHistory(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + var ( + myContractBech32Addr = RandomBech32AccountAddress(t) + otherBech32Addr = RandomBech32AccountAddress(t) + ) + + specs := map[string]struct { + srcHistory []types.ContractCodeHistoryEntry + req types.QueryContractHistoryRequest + expContent []types.ContractCodeHistoryEntry + }{ + "response with internal fields cleared": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeGenesis, + CodeID: firstCodeID, + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + Msg: []byte(`"init message"`), + }}, + req: types.QueryContractHistoryRequest{Address: myContractBech32Addr}, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeGenesis, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + }}, + }, + "response with multiple entries": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + Msg: []byte(`"init message"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4}, + Msg: []byte(`"migrate message 1"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 3, + Updated: &types.AbsoluteTxPosition{BlockHeight: 5, TxIndex: 6}, + Msg: []byte(`"migrate message 2"`), + }}, + req: types.QueryContractHistoryRequest{Address: myContractBech32Addr}, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Msg: []byte(`"migrate message 1"`), + Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4}, + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 3, + Msg: []byte(`"migrate message 2"`), + Updated: &types.AbsoluteTxPosition{BlockHeight: 5, TxIndex: 6}, + }}, + }, + "with pagination offset": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + Msg: []byte(`"init message"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4}, + Msg: []byte(`"migrate message 1"`), + }}, + req: types.QueryContractHistoryRequest{ + Address: myContractBech32Addr, + Pagination: &query.PageRequest{ + Offset: 1, + }, + }, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Msg: []byte(`"migrate message 1"`), + Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4}, + }}, + }, + "with pagination limit": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + Msg: []byte(`"init message"`), + }, { + Operation: types.ContractCodeHistoryOperationTypeMigrate, + CodeID: 2, + Updated: &types.AbsoluteTxPosition{BlockHeight: 3, TxIndex: 4}, + Msg: []byte(`"migrate message 1"`), + }}, + req: types.QueryContractHistoryRequest{ + Address: myContractBech32Addr, + Pagination: &query.PageRequest{ + Limit: 1, + }, + }, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeInit, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + Updated: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2}, + }}, + }, + "unknown contract address": { + req: types.QueryContractHistoryRequest{Address: otherBech32Addr}, + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.ContractCodeHistoryOperationTypeGenesis, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }}, + expContent: nil, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + xCtx, _ := ctx.CacheContext() + + cAddr, _ := sdk.AccAddressFromBech32(myContractBech32Addr) + keeper.appendToContractHistory(xCtx, cAddr, spec.srcHistory...) + + // when + q := Querier(keeper) + got, err := q.ContractHistory(sdk.WrapSDKContext(xCtx), &spec.req) + + // then + if spec.expContent == nil { + require.Error(t, types.ErrEmpty) + return + } + require.NoError(t, err) + assert.Equal(t, spec.expContent, got.Entries) + }) + } +} + +func TestQueryCodeList(t *testing.T) { + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + specs := map[string]struct { + storedCodeIDs []uint64 + req types.QueryCodesRequest + expCodeIDs []uint64 + }{ + "none": {}, + "no gaps": { + storedCodeIDs: []uint64{1, 2, 3}, + expCodeIDs: []uint64{1, 2, 3}, + }, + "with gaps": { + storedCodeIDs: []uint64{2, 4, 6}, + expCodeIDs: []uint64{2, 4, 6}, + }, + "with pagination offset": { + storedCodeIDs: []uint64{1, 2, 3}, + req: types.QueryCodesRequest{ + Pagination: &query.PageRequest{ + Offset: 1, + }, + }, + expCodeIDs: []uint64{2, 3}, + }, + "with pagination limit": { + storedCodeIDs: []uint64{1, 2, 3}, + req: types.QueryCodesRequest{ + Pagination: &query.PageRequest{ + Limit: 2, + }, + }, + expCodeIDs: []uint64{1, 2}, + }, + "with pagination next key": { + storedCodeIDs: []uint64{1, 2, 3}, + req: types.QueryCodesRequest{ + Pagination: &query.PageRequest{ + Key: fromBase64("AAAAAAAAAAI="), + }, + }, + expCodeIDs: []uint64{2, 3}, + }, + } + + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + xCtx, _ := ctx.CacheContext() + + for _, codeID := range spec.storedCodeIDs { + require.NoError(t, keeper.importCode(xCtx, codeID, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + } + // when + q := Querier(keeper) + got, err := q.Codes(sdk.WrapSDKContext(xCtx), &spec.req) + + // then + require.NoError(t, err) + require.NotNil(t, got.CodeInfos) + require.Len(t, got.CodeInfos, len(spec.expCodeIDs)) + for i, exp := range spec.expCodeIDs { + assert.EqualValues(t, exp, got.CodeInfos[i].CodeID) + } + }) + } +} + +func TestQueryContractInfo(t *testing.T) { + var ( + contractAddr = RandomAccountAddress(t) + anyDate = time.Now().UTC() + ) + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + // register an example extension. must be protobuf + keepers.EncodingConfig.InterfaceRegistry.RegisterImplementations( + (*types.ContractInfoExtension)(nil), + &govtypes.Proposal{}, + ) + govtypes.RegisterInterfaces(keepers.EncodingConfig.InterfaceRegistry) + + k := keepers.WasmKeeper + querier := NewGrpcQuerier(k.cdc, k.storeKey, k, k.queryGasLimit) + myExtension := func(info *types.ContractInfo) { + // abuse gov proposal as a random protobuf extension with an Any type + myExt, err := govtypes.NewProposal(&govtypes.TextProposal{Title: "foo", Description: "bar"}, 1, anyDate, anyDate) + require.NoError(t, err) + myExt.TotalDeposit = nil + info.SetExtension(&myExt) + } + specs := map[string]struct { + src *types.QueryContractInfoRequest + stored types.ContractInfo + expRsp *types.QueryContractInfoResponse + expErr bool + }{ + "found": { + src: &types.QueryContractInfoRequest{Address: contractAddr.String()}, + stored: types.ContractInfoFixture(), + expRsp: &types.QueryContractInfoResponse{ + Address: contractAddr.String(), + ContractInfo: types.ContractInfoFixture(), + }, + }, + "with extension": { + src: &types.QueryContractInfoRequest{Address: contractAddr.String()}, + stored: types.ContractInfoFixture(myExtension), + expRsp: &types.QueryContractInfoResponse{ + Address: contractAddr.String(), + ContractInfo: types.ContractInfoFixture(myExtension), + }, + }, + "not found": { + src: &types.QueryContractInfoRequest{Address: RandomBech32AccountAddress(t)}, + stored: types.ContractInfoFixture(), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + xCtx, _ := ctx.CacheContext() + k.storeContractInfo(xCtx, contractAddr, &spec.stored) + // when + gotRsp, gotErr := querier.ContractInfo(sdk.WrapSDKContext(xCtx), spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expRsp, gotRsp) + }) + } +} + +func TestQueryPinnedCodes(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + exampleContract1 := InstantiateHackatomExampleContract(t, ctx, keepers) + exampleContract2 := InstantiateIBCReflectContract(t, ctx, keepers) + require.NoError(t, keeper.pinCode(ctx, exampleContract1.CodeID)) + require.NoError(t, keeper.pinCode(ctx, exampleContract2.CodeID)) + + q := Querier(keeper) + specs := map[string]struct { + srcQuery *types.QueryPinnedCodesRequest + expCodeIDs []uint64 + expErr *sdkErrors.Error + }{ + "query all": { + srcQuery: &types.QueryPinnedCodesRequest{}, + expCodeIDs: []uint64{exampleContract1.CodeID, exampleContract2.CodeID}, + }, + "with pagination offset": { + srcQuery: &types.QueryPinnedCodesRequest{ + Pagination: &query.PageRequest{ + Offset: 1, + }, + }, + expCodeIDs: []uint64{exampleContract2.CodeID}, + }, + "with pagination limit": { + srcQuery: &types.QueryPinnedCodesRequest{ + Pagination: &query.PageRequest{ + Limit: 1, + }, + }, + expCodeIDs: []uint64{exampleContract1.CodeID}, + }, + "with pagination next key": { + srcQuery: &types.QueryPinnedCodesRequest{ + Pagination: &query.PageRequest{ + Key: fromBase64("AAAAAAAAAAM="), + }, + }, + expCodeIDs: []uint64{exampleContract2.CodeID}, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := q.PinnedCodes(sdk.WrapSDKContext(ctx), spec.srcQuery) + require.True(t, spec.expErr.Is(err), err) + if spec.expErr != nil { + return + } + require.NotNil(t, got) + assert.Equal(t, spec.expCodeIDs, got.CodeIDs) + }) + } +} + +func TestQueryParams(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + q := Querier(keeper) + + paramsResponse, err := q.Params(sdk.WrapSDKContext(ctx), &types.QueryParamsRequest{}) + require.NoError(t, err) + require.NotNil(t, paramsResponse) + + defaultParams := types.DefaultParams() + + require.Equal(t, paramsResponse.Params.CodeUploadAccess, defaultParams.CodeUploadAccess) + require.Equal(t, paramsResponse.Params.InstantiateDefaultPermission, defaultParams.InstantiateDefaultPermission) + + keeper.SetParams(ctx, types.Params{ + CodeUploadAccess: types.AllowNobody, + InstantiateDefaultPermission: types.AccessTypeNobody, + }) + + paramsResponse, err = q.Params(sdk.WrapSDKContext(ctx), &types.QueryParamsRequest{}) + require.NoError(t, err) + require.NotNil(t, paramsResponse) + + require.Equal(t, paramsResponse.Params.CodeUploadAccess, types.AllowNobody) + require.Equal(t, paramsResponse.Params.InstantiateDefaultPermission, types.AccessTypeNobody) +} + +func TestQueryCodeInfo(t *testing.T) { + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + anyAddress, err := sdk.AccAddressFromBech32("cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz") + require.NoError(t, err) + specs := map[string]struct { + codeId uint64 + accessConfig types.AccessConfig + }{ + "everybody": { + codeId: 1, + accessConfig: types.AllowEverybody, + }, + "nobody": { + codeId: 10, + accessConfig: types.AllowNobody, + }, + "with_address": { + codeId: 20, + accessConfig: types.AccessTypeOnlyAddress.With(anyAddress), + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + codeInfo := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + codeInfo.InstantiateConfig = spec.accessConfig + require.NoError(t, keeper.importCode(ctx, spec.codeId, + codeInfo, + wasmCode), + ) + + q := Querier(keeper) + got, err := q.Code(sdk.WrapSDKContext(ctx), &types.QueryCodeRequest{ + CodeId: spec.codeId, + }) + require.NoError(t, err) + expectedResponse := &types.QueryCodeResponse{ + CodeInfoResponse: &types.CodeInfoResponse{ + CodeID: spec.codeId, + Creator: codeInfo.Creator, + DataHash: codeInfo.CodeHash, + InstantiatePermission: spec.accessConfig, + }, + Data: wasmCode, + } + require.NotNil(t, got.CodeInfoResponse) + require.EqualValues(t, expectedResponse, got) + }) + } +} + +func TestQueryCodeInfoList(t *testing.T) { + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + keeper := keepers.WasmKeeper + + anyAddress, err := sdk.AccAddressFromBech32("cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz") + require.NoError(t, err) + codeInfoWithConfig := func(accessConfig types.AccessConfig) types.CodeInfo { + codeInfo := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + codeInfo.InstantiateConfig = accessConfig + return codeInfo + } + + codes := []struct { + name string + codeId uint64 + codeInfo types.CodeInfo + }{ + { + name: "everybody", + codeId: 1, + codeInfo: codeInfoWithConfig(types.AllowEverybody), + }, + { + codeId: 10, + name: "nobody", + codeInfo: codeInfoWithConfig(types.AllowNobody), + }, + { + name: "with_address", + codeId: 20, + codeInfo: codeInfoWithConfig(types.AccessTypeOnlyAddress.With(anyAddress)), + }, + } + + allCodesResponse := make([]types.CodeInfoResponse, 0) + for _, code := range codes { + t.Run(fmt.Sprintf("import_%s", code.name), func(t *testing.T) { + require.NoError(t, keeper.importCode(ctx, code.codeId, + code.codeInfo, + wasmCode), + ) + }) + + allCodesResponse = append(allCodesResponse, types.CodeInfoResponse{ + CodeID: code.codeId, + Creator: code.codeInfo.Creator, + DataHash: code.codeInfo.CodeHash, + InstantiatePermission: code.codeInfo.InstantiateConfig, + }) + } + q := Querier(keeper) + got, err := q.Codes(sdk.WrapSDKContext(ctx), &types.QueryCodesRequest{ + Pagination: &query.PageRequest{ + Limit: 3, + }, + }) + require.NoError(t, err) + require.Len(t, got.CodeInfos, 3) + require.EqualValues(t, allCodesResponse, got.CodeInfos) +} + +func TestQueryContractsByCreatorList(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + anyAddr := keepers.Faucet.NewFundedRandomAccount(ctx, topUp...) + + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, wasmCode, nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: anyAddr, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + // manage some realistic block settings + var h int64 = 10 + setBlock := func(ctx sdk.Context, height int64) sdk.Context { + ctx = ctx.WithBlockHeight(height) + meter := sdk.NewGasMeter(1000000) + ctx = ctx.WithGasMeter(meter) + ctx = ctx.WithBlockGasMeter(meter) + return ctx + } + + var allExpecedContracts []string + // create 10 contracts with real block/gas setup + for i := 0; i < 10; i++ { + ctx = setBlock(ctx, h) + h++ + contract, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, fmt.Sprintf("contract %d", i), topUp) + allExpecedContracts = append(allExpecedContracts, contract.String()) + require.NoError(t, err) + } + + specs := map[string]struct { + srcQuery *types.QueryContractsByCreatorRequest + expContractAddr []string + expErr error + }{ + "query all": { + srcQuery: &types.QueryContractsByCreatorRequest{ + CreatorAddress: creator.String(), + }, + expContractAddr: allExpecedContracts, + expErr: nil, + }, + "with pagination offset": { + srcQuery: &types.QueryContractsByCreatorRequest{ + CreatorAddress: creator.String(), + Pagination: &query.PageRequest{ + Offset: 1, + }, + }, + expContractAddr: allExpecedContracts[1:], + expErr: nil, + }, + "with pagination limit": { + srcQuery: &types.QueryContractsByCreatorRequest{ + CreatorAddress: creator.String(), + Pagination: &query.PageRequest{ + Limit: 1, + }, + }, + expContractAddr: allExpecedContracts[0:1], + expErr: nil, + }, + "nil creator": { + srcQuery: &types.QueryContractsByCreatorRequest{ + Pagination: &query.PageRequest{}, + }, + expContractAddr: allExpecedContracts, + expErr: errors.New("empty address string is not allowed"), + }, + "nil req": { + srcQuery: nil, + expContractAddr: allExpecedContracts, + expErr: status.Error(codes.InvalidArgument, "empty request"), + }, + } + + q := Querier(keepers.WasmKeeper) + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := q.ContractsByCreator(sdk.WrapSDKContext(ctx), spec.srcQuery) + + if spec.expErr != nil { + require.Equal(t, spec.expErr, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, spec.expContractAddr, got.ContractAddresses) + }) + } +} + +func fromBase64(s string) []byte { + r, err := base64.StdEncoding.DecodeString(s) + if err != nil { + panic(err) + } + return r +} diff --git a/x/wasm/keeper/query_plugins.go b/x/wasm/keeper/query_plugins.go new file mode 100644 index 00000000..0bcc4c1e --- /dev/null +++ b/x/wasm/keeper/query_plugins.go @@ -0,0 +1,614 @@ +package keeper + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + abci "github.com/tendermint/tendermint/abci/types" + + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + + "github.com/cerc-io/laconicd/x/wasm/types" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +type QueryHandler struct { + Ctx sdk.Context + Plugins WasmVMQueryHandler + Caller sdk.AccAddress + gasRegister GasRegister +} + +func NewQueryHandler(ctx sdk.Context, vmQueryHandler WasmVMQueryHandler, caller sdk.AccAddress, gasRegister GasRegister) QueryHandler { + return QueryHandler{ + Ctx: ctx, + Plugins: vmQueryHandler, + Caller: caller, + gasRegister: gasRegister, + } +} + +type GRPCQueryRouter interface { + Route(path string) baseapp.GRPCQueryHandler +} + +// -- end baseapp interfaces -- + +var _ wasmvmtypes.Querier = QueryHandler{} + +func (q QueryHandler) Query(request wasmvmtypes.QueryRequest, gasLimit uint64) ([]byte, error) { + // set a limit for a subCtx + sdkGas := q.gasRegister.FromWasmVMGas(gasLimit) + // discard all changes/ events in subCtx by not committing the cached context + subCtx, _ := q.Ctx.WithGasMeter(sdk.NewGasMeter(sdkGas)).CacheContext() + + // make sure we charge the higher level context even on panic + defer func() { + q.Ctx.GasMeter().ConsumeGas(subCtx.GasMeter().GasConsumed(), "contract sub-query") + }() + + res, err := q.Plugins.HandleQuery(subCtx, q.Caller, request) + if err == nil { + // short-circuit, the rest is dealing with handling existing errors + return res, nil + } + + // special mappings to wasmvm system error (which are not redacted) + var wasmvmErr types.WasmVMErrorable + if ok := errors.As(err, &wasmvmErr); ok { + err = wasmvmErr.ToWasmVMError() + } + + // Issue #759 - we don't return error string for worries of non-determinism + return nil, redactError(err) +} + +func (q QueryHandler) GasConsumed() uint64 { + return q.Ctx.GasMeter().GasConsumed() +} + +type CustomQuerier func(ctx sdk.Context, request json.RawMessage) ([]byte, error) + +type QueryPlugins struct { + Bank func(ctx sdk.Context, request *wasmvmtypes.BankQuery) ([]byte, error) + Custom CustomQuerier + IBC func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error) + Staking func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) + Stargate func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) + Wasm func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error) +} + +type contractMetaDataSource interface { + GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo +} + +type wasmQueryKeeper interface { + contractMetaDataSource + GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo + QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte + QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) + IsPinnedCode(ctx sdk.Context, codeID uint64) bool +} + +func DefaultQueryPlugins( + bank types.BankViewKeeper, + staking types.StakingKeeper, + distKeeper types.DistributionKeeper, + channelKeeper types.ChannelKeeper, + wasm wasmQueryKeeper, +) QueryPlugins { + return QueryPlugins{ + Bank: BankQuerier(bank), + Custom: NoCustomQuerier, + IBC: IBCQuerier(wasm, channelKeeper), + Staking: StakingQuerier(staking, distKeeper), + Stargate: RejectStargateQuerier(), + Wasm: WasmQuerier(wasm), + } +} + +func (e QueryPlugins) Merge(o *QueryPlugins) QueryPlugins { + // only update if this is non-nil and then only set values + if o == nil { + return e + } + if o.Bank != nil { + e.Bank = o.Bank + } + if o.Custom != nil { + e.Custom = o.Custom + } + if o.IBC != nil { + e.IBC = o.IBC + } + if o.Staking != nil { + e.Staking = o.Staking + } + if o.Stargate != nil { + e.Stargate = o.Stargate + } + if o.Wasm != nil { + e.Wasm = o.Wasm + } + return e +} + +// HandleQuery executes the requested query +func (e QueryPlugins) HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + // do the query + if request.Bank != nil { + return e.Bank(ctx, request.Bank) + } + if request.Custom != nil { + return e.Custom(ctx, request.Custom) + } + if request.IBC != nil { + return e.IBC(ctx, caller, request.IBC) + } + if request.Staking != nil { + return e.Staking(ctx, request.Staking) + } + if request.Stargate != nil { + return e.Stargate(ctx, request.Stargate) + } + if request.Wasm != nil { + return e.Wasm(ctx, request.Wasm) + } + return nil, wasmvmtypes.Unknown{} +} + +func BankQuerier(bankKeeper types.BankViewKeeper) func(ctx sdk.Context, request *wasmvmtypes.BankQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.BankQuery) ([]byte, error) { + if request.AllBalances != nil { + addr, err := sdk.AccAddressFromBech32(request.AllBalances.Address) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.AllBalances.Address) + } + coins := bankKeeper.GetAllBalances(ctx, addr) + res := wasmvmtypes.AllBalancesResponse{ + Amount: ConvertSdkCoinsToWasmCoins(coins), + } + return json.Marshal(res) + } + if request.Balance != nil { + addr, err := sdk.AccAddressFromBech32(request.Balance.Address) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Balance.Address) + } + coin := bankKeeper.GetBalance(ctx, addr, request.Balance.Denom) + res := wasmvmtypes.BalanceResponse{ + Amount: wasmvmtypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + }, + } + return json.Marshal(res) + } + if request.Supply != nil { + coin := bankKeeper.GetSupply(ctx, request.Supply.Denom) + res := wasmvmtypes.SupplyResponse{ + Amount: wasmvmtypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + }, + } + return json.Marshal(res) + } + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown BankQuery variant"} + } +} + +func NoCustomQuerier(sdk.Context, json.RawMessage) ([]byte, error) { + return nil, wasmvmtypes.UnsupportedRequest{Kind: "custom"} +} + +func IBCQuerier(wasm contractMetaDataSource, channelKeeper types.ChannelKeeper) func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error) { + return func(ctx sdk.Context, caller sdk.AccAddress, request *wasmvmtypes.IBCQuery) ([]byte, error) { + if request.PortID != nil { + contractInfo := wasm.GetContractInfo(ctx, caller) + res := wasmvmtypes.PortIDResponse{ + PortID: contractInfo.IBCPortID, + } + return json.Marshal(res) + } + if request.ListChannels != nil { + portID := request.ListChannels.PortID + channels := make(wasmvmtypes.IBCChannels, 0) + channelKeeper.IterateChannels(ctx, func(ch channeltypes.IdentifiedChannel) bool { + // it must match the port and be in open state + if (portID == "" || portID == ch.PortId) && ch.State == channeltypes.OPEN { + newChan := wasmvmtypes.IBCChannel{ + Endpoint: wasmvmtypes.IBCEndpoint{ + PortID: ch.PortId, + ChannelID: ch.ChannelId, + }, + CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{ + PortID: ch.Counterparty.PortId, + ChannelID: ch.Counterparty.ChannelId, + }, + Order: ch.Ordering.String(), + Version: ch.Version, + ConnectionID: ch.ConnectionHops[0], + } + channels = append(channels, newChan) + } + return false + }) + res := wasmvmtypes.ListChannelsResponse{ + Channels: channels, + } + return json.Marshal(res) + } + if request.Channel != nil { + channelID := request.Channel.ChannelID + portID := request.Channel.PortID + if portID == "" { + contractInfo := wasm.GetContractInfo(ctx, caller) + portID = contractInfo.IBCPortID + } + got, found := channelKeeper.GetChannel(ctx, portID, channelID) + var channel *wasmvmtypes.IBCChannel + // it must be in open state + if found && got.State == channeltypes.OPEN { + channel = &wasmvmtypes.IBCChannel{ + Endpoint: wasmvmtypes.IBCEndpoint{ + PortID: portID, + ChannelID: channelID, + }, + CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{ + PortID: got.Counterparty.PortId, + ChannelID: got.Counterparty.ChannelId, + }, + Order: got.Ordering.String(), + Version: got.Version, + ConnectionID: got.ConnectionHops[0], + } + } + res := wasmvmtypes.ChannelResponse{ + Channel: channel, + } + return json.Marshal(res) + } + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown IBCQuery variant"} + } +} + +// RejectStargateQuerier rejects all stargate queries +func RejectStargateQuerier() func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + return nil, wasmvmtypes.UnsupportedRequest{Kind: "Stargate queries are disabled"} + } +} + +// AcceptedStargateQueries define accepted Stargate queries as a map with path as key and response type as value. +// For example: +// acceptList["/cosmos.auth.v1beta1.Query/Account"]= &authtypes.QueryAccountResponse{} +type AcceptedStargateQueries map[string]codec.ProtoMarshaler + +// AcceptListStargateQuerier supports a preconfigured set of stargate queries only. +// All arguments must be non nil. +// +// Warning: Chains need to test and maintain their accept list carefully. +// There were critical consensus breaking issues in the past with non-deterministic behaviour in the SDK. +// +// This queries can be set via WithQueryPlugins option in the wasm keeper constructor: +// WithQueryPlugins(&QueryPlugins{Stargate: AcceptListStargateQuerier(acceptList, queryRouter, codec)}) +func AcceptListStargateQuerier(acceptList AcceptedStargateQueries, queryRouter GRPCQueryRouter, codec codec.Codec) func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { + protoResponse, accepted := acceptList[request.Path] + if !accepted { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)} + } + + route := queryRouter.Route(request.Path) + if route == nil { + return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)} + } + + res, err := route(ctx, abci.RequestQuery{ + Data: request.Data, + Path: request.Path, + }) + if err != nil { + return nil, err + } + + return ConvertProtoToJSONMarshal(codec, protoResponse, res.Value) + } +} + +func StakingQuerier(keeper types.StakingKeeper, distKeeper types.DistributionKeeper) func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) { + if request.BondedDenom != nil { + denom := keeper.BondDenom(ctx) + res := wasmvmtypes.BondedDenomResponse{ + Denom: denom, + } + return json.Marshal(res) + } + if request.AllValidators != nil { + validators := keeper.GetBondedValidatorsByPower(ctx) + // validators := keeper.GetAllValidators(ctx) + wasmVals := make([]wasmvmtypes.Validator, len(validators)) + for i, v := range validators { + wasmVals[i] = wasmvmtypes.Validator{ + Address: v.OperatorAddress, + Commission: v.Commission.Rate.String(), + MaxCommission: v.Commission.MaxRate.String(), + MaxChangeRate: v.Commission.MaxChangeRate.String(), + } + } + res := wasmvmtypes.AllValidatorsResponse{ + Validators: wasmVals, + } + return json.Marshal(res) + } + if request.Validator != nil { + valAddr, err := sdk.ValAddressFromBech32(request.Validator.Address) + if err != nil { + return nil, err + } + v, found := keeper.GetValidator(ctx, valAddr) + res := wasmvmtypes.ValidatorResponse{} + if found { + res.Validator = &wasmvmtypes.Validator{ + Address: v.OperatorAddress, + Commission: v.Commission.Rate.String(), + MaxCommission: v.Commission.MaxRate.String(), + MaxChangeRate: v.Commission.MaxChangeRate.String(), + } + } + return json.Marshal(res) + } + if request.AllDelegations != nil { + delegator, err := sdk.AccAddressFromBech32(request.AllDelegations.Delegator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.AllDelegations.Delegator) + } + sdkDels := keeper.GetAllDelegatorDelegations(ctx, delegator) + delegations, err := sdkToDelegations(ctx, keeper, sdkDels) + if err != nil { + return nil, err + } + res := wasmvmtypes.AllDelegationsResponse{ + Delegations: delegations, + } + return json.Marshal(res) + } + if request.Delegation != nil { + delegator, err := sdk.AccAddressFromBech32(request.Delegation.Delegator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Delegation.Delegator) + } + validator, err := sdk.ValAddressFromBech32(request.Delegation.Validator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Delegation.Validator) + } + + var res wasmvmtypes.DelegationResponse + d, found := keeper.GetDelegation(ctx, delegator, validator) + if found { + res.Delegation, err = sdkToFullDelegation(ctx, keeper, distKeeper, d) + if err != nil { + return nil, err + } + } + return json.Marshal(res) + } + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown Staking variant"} + } +} + +func sdkToDelegations(ctx sdk.Context, keeper types.StakingKeeper, delegations []stakingtypes.Delegation) (wasmvmtypes.Delegations, error) { + result := make([]wasmvmtypes.Delegation, len(delegations)) + bondDenom := keeper.BondDenom(ctx) + + for i, d := range delegations { + delAddr, err := sdk.AccAddressFromBech32(d.DelegatorAddress) + if err != nil { + return nil, sdkerrors.Wrap(err, "delegator address") + } + valAddr, err := sdk.ValAddressFromBech32(d.ValidatorAddress) + if err != nil { + return nil, sdkerrors.Wrap(err, "validator address") + } + + // shares to amount logic comes from here: + // https://github.com/cosmos/cosmos-sdk/blob/v0.38.3/x/staking/keeper/querier.go#L404 + val, found := keeper.GetValidator(ctx, valAddr) + if !found { + return nil, sdkerrors.Wrap(stakingtypes.ErrNoValidatorFound, "can't load validator for delegation") + } + amount := sdk.NewCoin(bondDenom, val.TokensFromShares(d.Shares).TruncateInt()) + + result[i] = wasmvmtypes.Delegation{ + Delegator: delAddr.String(), + Validator: valAddr.String(), + Amount: ConvertSdkCoinToWasmCoin(amount), + } + } + return result, nil +} + +func sdkToFullDelegation(ctx sdk.Context, keeper types.StakingKeeper, distKeeper types.DistributionKeeper, delegation stakingtypes.Delegation) (*wasmvmtypes.FullDelegation, error) { + delAddr, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress) + if err != nil { + return nil, sdkerrors.Wrap(err, "delegator address") + } + valAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress) + if err != nil { + return nil, sdkerrors.Wrap(err, "validator address") + } + val, found := keeper.GetValidator(ctx, valAddr) + if !found { + return nil, sdkerrors.Wrap(stakingtypes.ErrNoValidatorFound, "can't load validator for delegation") + } + bondDenom := keeper.BondDenom(ctx) + amount := sdk.NewCoin(bondDenom, val.TokensFromShares(delegation.Shares).TruncateInt()) + + delegationCoins := ConvertSdkCoinToWasmCoin(amount) + + // FIXME: this is very rough but better than nothing... + // https://github.com/CosmWasm/wasmd/issues/282 + // if this (val, delegate) pair is receiving a redelegation, it cannot redelegate more + // otherwise, it can redelegate the full amount + // (there are cases of partial funds redelegated, but this is a start) + redelegateCoins := wasmvmtypes.NewCoin(0, bondDenom) + if !keeper.HasReceivingRedelegation(ctx, delAddr, valAddr) { + redelegateCoins = delegationCoins + } + + // FIXME: make a cleaner way to do this (modify the sdk) + // we need the info from `distKeeper.calculateDelegationRewards()`, but it is not public + // neither is `queryDelegationRewards(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper)` + // so we go through the front door of the querier.... + accRewards, err := getAccumulatedRewards(ctx, distKeeper, delegation) + if err != nil { + return nil, err + } + + return &wasmvmtypes.FullDelegation{ + Delegator: delAddr.String(), + Validator: valAddr.String(), + Amount: delegationCoins, + AccumulatedRewards: accRewards, + CanRedelegate: redelegateCoins, + }, nil +} + +// FIXME: simplify this enormously when +// https://github.com/cosmos/cosmos-sdk/issues/7466 is merged +func getAccumulatedRewards(ctx sdk.Context, distKeeper types.DistributionKeeper, delegation stakingtypes.Delegation) ([]wasmvmtypes.Coin, error) { + // Try to get *delegator* reward info! + params := distributiontypes.QueryDelegationRewardsRequest{ + DelegatorAddress: delegation.DelegatorAddress, + ValidatorAddress: delegation.ValidatorAddress, + } + cache, _ := ctx.CacheContext() + qres, err := distKeeper.DelegationRewards(sdk.WrapSDKContext(cache), ¶ms) + if err != nil { + return nil, err + } + + // now we have it, convert it into wasmvm types + rewards := make([]wasmvmtypes.Coin, len(qres.Rewards)) + for i, r := range qres.Rewards { + rewards[i] = wasmvmtypes.Coin{ + Denom: r.Denom, + Amount: r.Amount.TruncateInt().String(), + } + } + return rewards, nil +} + +func WasmQuerier(k wasmQueryKeeper) func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmvmtypes.WasmQuery) ([]byte, error) { + switch { + case request.Smart != nil: + addr, err := sdk.AccAddressFromBech32(request.Smart.ContractAddr) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Smart.ContractAddr) + } + msg := types.RawContractMessage(request.Smart.Msg) + if err := msg.ValidateBasic(); err != nil { + return nil, sdkerrors.Wrap(err, "json msg") + } + return k.QuerySmart(ctx, addr, msg) + case request.Raw != nil: + addr, err := sdk.AccAddressFromBech32(request.Raw.ContractAddr) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Raw.ContractAddr) + } + return k.QueryRaw(ctx, addr, request.Raw.Key), nil + case request.ContractInfo != nil: + contractAddr := request.ContractInfo.ContractAddr + addr, err := sdk.AccAddressFromBech32(contractAddr) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, contractAddr) + } + info := k.GetContractInfo(ctx, addr) + if info == nil { + return nil, types.ErrNoSuchContractFn(contractAddr). + Wrapf("address %s", contractAddr) + } + res := wasmvmtypes.ContractInfoResponse{ + CodeID: info.CodeID, + Creator: info.Creator, + Admin: info.Admin, + Pinned: k.IsPinnedCode(ctx, info.CodeID), + IBCPort: info.IBCPortID, + } + return json.Marshal(res) + case request.CodeInfo != nil: + if request.CodeInfo.CodeID == 0 { + return nil, types.ErrEmpty.Wrap("code id") + } + info := k.GetCodeInfo(ctx, request.CodeInfo.CodeID) + if info == nil { + return nil, types.ErrNoSuchCodeFn(request.CodeInfo.CodeID). + Wrapf("code id %d", request.CodeInfo.CodeID) + } + + res := wasmvmtypes.CodeInfoResponse{ + CodeID: request.CodeInfo.CodeID, + Creator: info.Creator, + Checksum: info.CodeHash, + } + return json.Marshal(res) + } + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown WasmQuery variant"} + } +} + +// ConvertSdkCoinsToWasmCoins covert sdk type to wasmvm coins type +func ConvertSdkCoinsToWasmCoins(coins []sdk.Coin) wasmvmtypes.Coins { + converted := make(wasmvmtypes.Coins, len(coins)) + for i, c := range coins { + converted[i] = ConvertSdkCoinToWasmCoin(c) + } + return converted +} + +// ConvertSdkCoinToWasmCoin covert sdk type to wasmvm coin type +func ConvertSdkCoinToWasmCoin(coin sdk.Coin) wasmvmtypes.Coin { + return wasmvmtypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + } +} + +// ConvertProtoToJSONMarshal unmarshals the given bytes into a proto message and then marshals it to json. +// This is done so that clients calling stargate queries do not need to define their own proto unmarshalers, +// being able to use response directly by json marshalling, which is supported in cosmwasm. +func ConvertProtoToJSONMarshal(cdc codec.Codec, protoResponse codec.ProtoMarshaler, bz []byte) ([]byte, error) { + // unmarshal binary into stargate response data structure + err := cdc.Unmarshal(bz, protoResponse) + if err != nil { + return nil, sdkerrors.Wrap(err, "to proto") + } + + bz, err = cdc.MarshalJSON(protoResponse) + if err != nil { + return nil, sdkerrors.Wrap(err, "to json") + } + + return bz, nil +} + +var _ WasmVMQueryHandler = WasmVMQueryHandlerFn(nil) + +// WasmVMQueryHandlerFn is a helper to construct a function based query handler. +type WasmVMQueryHandlerFn func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) + +// HandleQuery delegates call into wrapped WasmVMQueryHandlerFn +func (w WasmVMQueryHandlerFn) HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + return w(ctx, caller, request) +} diff --git a/x/wasm/keeper/query_plugins_test.go b/x/wasm/keeper/query_plugins_test.go new file mode 100644 index 00000000..da2d5f6b --- /dev/null +++ b/x/wasm/keeper/query_plugins_test.go @@ -0,0 +1,825 @@ +package keeper_test + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "testing" + "time" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/query" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "github.com/cerc-io/laconicd/app" + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestIBCQuerier(t *testing.T) { + myExampleChannels := []channeltypes.IdentifiedChannel{ + // this is returned + { + State: channeltypes.OPEN, + Ordering: channeltypes.ORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "counterPartyPortID", + ChannelId: "counterPartyChannelID", + }, + ConnectionHops: []string{"one"}, + Version: "v1", + PortId: "myPortID", + ChannelId: "myChannelID", + }, + // this is filtered out + { + State: channeltypes.INIT, + Ordering: channeltypes.UNORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "foobar", + }, + ConnectionHops: []string{"one"}, + Version: "initversion", + PortId: "initPortID", + ChannelId: "initChannelID", + }, + // this is returned + { + State: channeltypes.OPEN, + Ordering: channeltypes.UNORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "otherCounterPartyPortID", + ChannelId: "otherCounterPartyChannelID", + }, + ConnectionHops: []string{"other", "second"}, + Version: "otherVersion", + PortId: "otherPortID", + ChannelId: "otherChannelID", + }, + // this is filtered out + { + State: channeltypes.CLOSED, + Ordering: channeltypes.ORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "super", + ChannelId: "duper", + }, + ConnectionHops: []string{"no-more"}, + Version: "closedVersion", + PortId: "closedPortID", + ChannelId: "closedChannelID", + }, + } + specs := map[string]struct { + srcQuery *wasmvmtypes.IBCQuery + wasmKeeper *mockWasmQueryKeeper + channelKeeper *wasmtesting.MockChannelKeeper + expJsonResult string + expErr *sdkerrors.Error + }{ + "query port id": { + srcQuery: &wasmvmtypes.IBCQuery{ + PortID: &wasmvmtypes.PortIDQuery{}, + }, + wasmKeeper: &mockWasmQueryKeeper{ + GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + return &types.ContractInfo{IBCPortID: "myIBCPortID"} + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{}, + expJsonResult: `{"port_id":"myIBCPortID"}`, + }, + "query list channels - all": { + srcQuery: &wasmvmtypes.IBCQuery{ + ListChannels: &wasmvmtypes.ListChannelsQuery{}, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + IterateChannelsFn: wasmtesting.MockChannelKeeperIterator(myExampleChannels), + }, + expJsonResult: `{ + "channels": [ + { + "endpoint": { + "port_id": "myPortID", + "channel_id": "myChannelID" + }, + "counterparty_endpoint": { + "port_id": "counterPartyPortID", + "channel_id": "counterPartyChannelID" + }, + "order": "ORDER_ORDERED", + "version": "v1", + "connection_id": "one" + }, + { + "endpoint": { + "port_id": "otherPortID", + "channel_id": "otherChannelID" + }, + "counterparty_endpoint": { + "port_id": "otherCounterPartyPortID", + "channel_id": "otherCounterPartyChannelID" + }, + "order": "ORDER_UNORDERED", + "version": "otherVersion", + "connection_id": "other" + } + ] +}`, + }, + "query list channels - filtered": { + srcQuery: &wasmvmtypes.IBCQuery{ + ListChannels: &wasmvmtypes.ListChannelsQuery{ + PortID: "otherPortID", + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + IterateChannelsFn: wasmtesting.MockChannelKeeperIterator(myExampleChannels), + }, + expJsonResult: `{ + "channels": [ + { + "endpoint": { + "port_id": "otherPortID", + "channel_id": "otherChannelID" + }, + "counterparty_endpoint": { + "port_id": "otherCounterPartyPortID", + "channel_id": "otherCounterPartyChannelID" + }, + "order": "ORDER_UNORDERED", + "version": "otherVersion", + "connection_id": "other" + } + ] +}`, + }, + "query list channels - filtered empty": { + srcQuery: &wasmvmtypes.IBCQuery{ + ListChannels: &wasmvmtypes.ListChannelsQuery{ + PortID: "none-existing", + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + IterateChannelsFn: wasmtesting.MockChannelKeeperIterator(myExampleChannels), + }, + expJsonResult: `{"channels": []}`, + }, + "query channel": { + srcQuery: &wasmvmtypes.IBCQuery{ + Channel: &wasmvmtypes.ChannelQuery{ + PortID: "myQueryPortID", + ChannelID: "myQueryChannelID", + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) { + return channeltypes.Channel{ + State: channeltypes.OPEN, + Ordering: channeltypes.UNORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "counterPartyPortID", + ChannelId: "otherCounterPartyChannelID", + }, + ConnectionHops: []string{"one"}, + Version: "version", + }, true + }, + }, + expJsonResult: `{ + "channel": { + "endpoint": { + "port_id": "myQueryPortID", + "channel_id": "myQueryChannelID" + }, + "counterparty_endpoint": { + "port_id": "counterPartyPortID", + "channel_id": "otherCounterPartyChannelID" + }, + "order": "ORDER_UNORDERED", + "version": "version", + "connection_id": "one" + } +}`, + }, + "query channel - without port set": { + srcQuery: &wasmvmtypes.IBCQuery{ + Channel: &wasmvmtypes.ChannelQuery{ + ChannelID: "myQueryChannelID", + }, + }, + wasmKeeper: &mockWasmQueryKeeper{ + GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + return &types.ContractInfo{IBCPortID: "myLoadedPortID"} + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) { + return channeltypes.Channel{ + State: channeltypes.OPEN, + Ordering: channeltypes.UNORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "counterPartyPortID", + ChannelId: "otherCounterPartyChannelID", + }, + ConnectionHops: []string{"one"}, + Version: "version", + }, true + }, + }, + expJsonResult: `{ + "channel": { + "endpoint": { + "port_id": "myLoadedPortID", + "channel_id": "myQueryChannelID" + }, + "counterparty_endpoint": { + "port_id": "counterPartyPortID", + "channel_id": "otherCounterPartyChannelID" + }, + "order": "ORDER_UNORDERED", + "version": "version", + "connection_id": "one" + } +}`, + }, + "query channel in init state": { + srcQuery: &wasmvmtypes.IBCQuery{ + Channel: &wasmvmtypes.ChannelQuery{ + PortID: "myQueryPortID", + ChannelID: "myQueryChannelID", + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) { + return channeltypes.Channel{ + State: channeltypes.INIT, + Ordering: channeltypes.UNORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "foobar", + }, + ConnectionHops: []string{"one"}, + Version: "initversion", + }, true + }, + }, + expJsonResult: "{}", + }, + "query channel in closed state": { + srcQuery: &wasmvmtypes.IBCQuery{ + Channel: &wasmvmtypes.ChannelQuery{ + PortID: "myQueryPortID", + ChannelID: "myQueryChannelID", + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) { + return channeltypes.Channel{ + State: channeltypes.CLOSED, + Ordering: channeltypes.ORDERED, + Counterparty: channeltypes.Counterparty{ + PortId: "super", + ChannelId: "duper", + }, + ConnectionHops: []string{"no-more"}, + Version: "closedVersion", + }, true + }, + }, + expJsonResult: "{}", + }, + "query channel - empty result": { + srcQuery: &wasmvmtypes.IBCQuery{ + Channel: &wasmvmtypes.ChannelQuery{ + PortID: "myQueryPortID", + ChannelID: "myQueryChannelID", + }, + }, + channelKeeper: &wasmtesting.MockChannelKeeper{ + GetChannelFn: func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) { + return channeltypes.Channel{}, false + }, + }, + expJsonResult: "{}", + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + h := keeper.IBCQuerier(spec.wasmKeeper, spec.channelKeeper) + gotResult, gotErr := h(sdk.Context{}, keeper.RandomAccountAddress(t), spec.srcQuery) + require.True(t, spec.expErr.Is(gotErr), "exp %v but got %#+v", spec.expErr, gotErr) + if spec.expErr != nil { + return + } + assert.JSONEq(t, spec.expJsonResult, string(gotResult), string(gotResult)) + }) + } +} + +func TestBankQuerierBalance(t *testing.T) { + mock := bankKeeperMock{GetBalanceFn: func(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { + return sdk.NewCoin(denom, sdk.NewInt(1)) + }} + + ctx := sdk.Context{} + q := keeper.BankQuerier(mock) + gotBz, gotErr := q(ctx, &wasmvmtypes.BankQuery{ + Balance: &wasmvmtypes.BalanceQuery{ + Address: keeper.RandomBech32AccountAddress(t), + Denom: "ALX", + }, + }) + require.NoError(t, gotErr) + var got wasmvmtypes.BalanceResponse + require.NoError(t, json.Unmarshal(gotBz, &got)) + exp := wasmvmtypes.BalanceResponse{ + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + } + assert.Equal(t, exp, got) +} + +func TestContractInfoWasmQuerier(t *testing.T) { + myValidContractAddr := keeper.RandomBech32AccountAddress(t) + myCreatorAddr := keeper.RandomBech32AccountAddress(t) + myAdminAddr := keeper.RandomBech32AccountAddress(t) + var ctx sdk.Context + + specs := map[string]struct { + req *wasmvmtypes.WasmQuery + mock mockWasmQueryKeeper + expRes wasmvmtypes.ContractInfoResponse + expErr bool + }{ + "all good": { + req: &wasmvmtypes.WasmQuery{ + ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr}, + }, + mock: mockWasmQueryKeeper{ + GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + val := types.ContractInfoFixture(func(i *types.ContractInfo) { + i.Admin, i.Creator, i.IBCPortID = myAdminAddr, myCreatorAddr, "myIBCPort" + }) + return &val + }, + IsPinnedCodeFn: func(ctx sdk.Context, codeID uint64) bool { return true }, + }, + expRes: wasmvmtypes.ContractInfoResponse{ + CodeID: 1, + Creator: myCreatorAddr, + Admin: myAdminAddr, + Pinned: true, + IBCPort: "myIBCPort", + }, + }, + "invalid addr": { + req: &wasmvmtypes.WasmQuery{ + ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: "not a valid addr"}, + }, + expErr: true, + }, + "unknown addr": { + req: &wasmvmtypes.WasmQuery{ + ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr}, + }, + mock: mockWasmQueryKeeper{GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + return nil + }}, + expErr: true, + }, + "not pinned": { + req: &wasmvmtypes.WasmQuery{ + ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr}, + }, + mock: mockWasmQueryKeeper{ + GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + val := types.ContractInfoFixture(func(i *types.ContractInfo) { + i.Admin, i.Creator = myAdminAddr, myCreatorAddr + }) + return &val + }, + IsPinnedCodeFn: func(ctx sdk.Context, codeID uint64) bool { return false }, + }, + expRes: wasmvmtypes.ContractInfoResponse{ + CodeID: 1, + Creator: myCreatorAddr, + Admin: myAdminAddr, + Pinned: false, + }, + }, + "without admin": { + req: &wasmvmtypes.WasmQuery{ + ContractInfo: &wasmvmtypes.ContractInfoQuery{ContractAddr: myValidContractAddr}, + }, + mock: mockWasmQueryKeeper{ + GetContractInfoFn: func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + val := types.ContractInfoFixture(func(i *types.ContractInfo) { + i.Creator = myCreatorAddr + }) + return &val + }, + IsPinnedCodeFn: func(ctx sdk.Context, codeID uint64) bool { return true }, + }, + expRes: wasmvmtypes.ContractInfoResponse{ + CodeID: 1, + Creator: myCreatorAddr, + Pinned: true, + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + q := keeper.WasmQuerier(spec.mock) + gotBz, gotErr := q(ctx, spec.req) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + var gotRes wasmvmtypes.ContractInfoResponse + require.NoError(t, json.Unmarshal(gotBz, &gotRes)) + assert.Equal(t, spec.expRes, gotRes) + }) + } +} + +func TestCodeInfoWasmQuerier(t *testing.T) { + myCreatorAddr := keeper.RandomBech32AccountAddress(t) + var ctx sdk.Context + + myRawChecksum := []byte("myHash78901234567890123456789012") + specs := map[string]struct { + req *wasmvmtypes.WasmQuery + mock mockWasmQueryKeeper + expRes wasmvmtypes.CodeInfoResponse + expErr bool + }{ + "all good": { + req: &wasmvmtypes.WasmQuery{ + CodeInfo: &wasmvmtypes.CodeInfoQuery{CodeID: 1}, + }, + mock: mockWasmQueryKeeper{ + GetCodeInfoFn: func(ctx sdk.Context, codeID uint64) *types.CodeInfo { + return &types.CodeInfo{ + CodeHash: myRawChecksum, + Creator: myCreatorAddr, + InstantiateConfig: types.AccessConfig{ + Permission: types.AccessTypeNobody, + Addresses: []string{myCreatorAddr}, + }, + } + }, + }, + expRes: wasmvmtypes.CodeInfoResponse{ + CodeID: 1, + Creator: myCreatorAddr, + Checksum: myRawChecksum, + }, + }, + "empty code id": { + req: &wasmvmtypes.WasmQuery{ + CodeInfo: &wasmvmtypes.CodeInfoQuery{}, + }, + expErr: true, + }, + "unknown code id": { + req: &wasmvmtypes.WasmQuery{ + CodeInfo: &wasmvmtypes.CodeInfoQuery{CodeID: 1}, + }, + mock: mockWasmQueryKeeper{ + GetCodeInfoFn: func(ctx sdk.Context, codeID uint64) *types.CodeInfo { + return nil + }, + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + q := keeper.WasmQuerier(spec.mock) + gotBz, gotErr := q(ctx, spec.req) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + var gotRes wasmvmtypes.CodeInfoResponse + require.NoError(t, json.Unmarshal(gotBz, &gotRes), string(gotBz)) + assert.Equal(t, spec.expRes, gotRes) + }) + } +} + +func TestQueryErrors(t *testing.T) { + specs := map[string]struct { + src error + expErr error + }{ + "no error": {}, + "no such contract": { + src: types.ErrNoSuchContractFn("contract-addr"), + expErr: wasmvmtypes.NoSuchContract{Addr: "contract-addr"}, + }, + "no such contract - wrapped": { + src: sdkerrors.Wrap(types.ErrNoSuchContractFn("contract-addr"), "my additional data"), + expErr: wasmvmtypes.NoSuchContract{Addr: "contract-addr"}, + }, + "no such code": { + src: types.ErrNoSuchCodeFn(123), + expErr: wasmvmtypes.NoSuchCode{CodeID: 123}, + }, + "no such code - wrapped": { + src: sdkerrors.Wrap(types.ErrNoSuchCodeFn(123), "my additional data"), + expErr: wasmvmtypes.NoSuchCode{CodeID: 123}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + mock := keeper.WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + return nil, spec.src + }) + ctx := sdk.Context{}.WithGasMeter(sdk.NewInfiniteGasMeter()).WithMultiStore(store.NewCommitMultiStore(dbm.NewMemDB())) + q := keeper.NewQueryHandler(ctx, mock, sdk.AccAddress{}, keeper.NewDefaultWasmGasRegister()) + _, gotErr := q.Query(wasmvmtypes.QueryRequest{}, 1) + assert.Equal(t, spec.expErr, gotErr) + }) + } +} + +func TestAcceptListStargateQuerier(t *testing.T) { + wasmApp := app.SetupWithEmptyStore(t) + ctx := wasmApp.NewUncachedContext(false, tmproto.Header{ChainID: "foo", Height: 1, Time: time.Now()}) + wasmApp.StakingKeeper.SetParams(ctx, stakingtypes.DefaultParams()) + + addrs := app.AddTestAddrs(wasmApp, ctx, 2, sdk.NewInt(1_000_000)) + accepted := keeper.AcceptedStargateQueries{ + "/cosmos.auth.v1beta1.Query/Account": &authtypes.QueryAccountResponse{}, + "/no/route/to/this": &authtypes.QueryAccountResponse{}, + } + + marshal := func(pb proto.Message) []byte { + b, err := proto.Marshal(pb) + require.NoError(t, err) + return b + } + + specs := map[string]struct { + req *wasmvmtypes.StargateQuery + expErr bool + expResp string + }{ + "in accept list - success result": { + req: &wasmvmtypes.StargateQuery{ + Path: "/cosmos.auth.v1beta1.Query/Account", + Data: marshal(&authtypes.QueryAccountRequest{Address: addrs[0].String()}), + }, + expResp: fmt.Sprintf(`{"account":{"@type":"/cosmos.auth.v1beta1.BaseAccount","address":%q,"pub_key":null,"account_number":"1","sequence":"0"}}`, addrs[0].String()), + }, + "in accept list - error result": { + req: &wasmvmtypes.StargateQuery{ + Path: "/cosmos.auth.v1beta1.Query/Account", + Data: marshal(&authtypes.QueryAccountRequest{Address: sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()).String()}), + }, + expErr: true, + }, + "not in accept list": { + req: &wasmvmtypes.StargateQuery{ + Path: "/cosmos.bank.v1beta1.Query/AllBalances", + Data: marshal(&banktypes.QueryAllBalancesRequest{Address: addrs[0].String()}), + }, + expErr: true, + }, + "unknown route": { + req: &wasmvmtypes.StargateQuery{ + Path: "/no/route/to/this", + Data: marshal(&banktypes.QueryAllBalancesRequest{Address: addrs[0].String()}), + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + q := keeper.AcceptListStargateQuerier(accepted, wasmApp.GRPCQueryRouter(), wasmApp.AppCodec()) + gotBz, gotErr := q(ctx, spec.req) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.JSONEq(t, spec.expResp, string(gotBz), string(gotBz)) + }) + } +} + +type mockWasmQueryKeeper struct { + GetContractInfoFn func(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo + QueryRawFn func(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte + QuerySmartFn func(ctx sdk.Context, contractAddr sdk.AccAddress, req types.RawContractMessage) ([]byte, error) + IsPinnedCodeFn func(ctx sdk.Context, codeID uint64) bool + GetCodeInfoFn func(ctx sdk.Context, codeID uint64) *types.CodeInfo +} + +func (m mockWasmQueryKeeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + if m.GetContractInfoFn == nil { + panic("not expected to be called") + } + return m.GetContractInfoFn(ctx, contractAddress) +} + +func (m mockWasmQueryKeeper) QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte { + if m.QueryRawFn == nil { + panic("not expected to be called") + } + return m.QueryRawFn(ctx, contractAddress, key) +} + +func (m mockWasmQueryKeeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) { + if m.QuerySmartFn == nil { + panic("not expected to be called") + } + return m.QuerySmartFn(ctx, contractAddr, req) +} + +func (m mockWasmQueryKeeper) IsPinnedCode(ctx sdk.Context, codeID uint64) bool { + if m.IsPinnedCodeFn == nil { + panic("not expected to be called") + } + return m.IsPinnedCodeFn(ctx, codeID) +} + +func (m mockWasmQueryKeeper) GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo { + if m.GetCodeInfoFn == nil { + panic("not expected to be called") + } + return m.GetCodeInfoFn(ctx, codeID) +} + +type bankKeeperMock struct { + GetSupplyFn func(ctx sdk.Context, denom string) sdk.Coin + GetBalanceFn func(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + GetAllBalancesFn func(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins +} + +func (m bankKeeperMock) GetSupply(ctx sdk.Context, denom string) sdk.Coin { + if m.GetSupplyFn == nil { + panic("not expected to be called") + } + return m.GetSupplyFn(ctx, denom) +} + +func (m bankKeeperMock) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin { + if m.GetBalanceFn == nil { + panic("not expected to be called") + } + return m.GetBalanceFn(ctx, addr, denom) +} + +func (m bankKeeperMock) GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { + if m.GetAllBalancesFn == nil { + panic("not expected to be called") + } + return m.GetAllBalancesFn(ctx, addr) +} + +func TestConvertProtoToJSONMarshal(t *testing.T) { + testCases := []struct { + name string + queryPath string + protoResponseStruct codec.ProtoMarshaler + originalResponse string + expectedProtoResponse codec.ProtoMarshaler + expectedError bool + }{ + { + name: "successful conversion from proto response to json marshalled response", + queryPath: "/cosmos.bank.v1beta1.Query/AllBalances", + originalResponse: "0a090a036261721202333012050a03666f6f", + protoResponseStruct: &banktypes.QueryAllBalancesResponse{}, + expectedProtoResponse: &banktypes.QueryAllBalancesResponse{ + Balances: sdk.NewCoins(sdk.NewCoin("bar", sdk.NewInt(30))), + Pagination: &query.PageResponse{ + NextKey: []byte("foo"), + }, + }, + }, + { + name: "invalid proto response struct", + queryPath: "/cosmos.bank.v1beta1.Query/AllBalances", + originalResponse: "0a090a036261721202333012050a03666f6f", + protoResponseStruct: &authtypes.QueryAccountResponse{}, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + originalVersionBz, err := hex.DecodeString(tc.originalResponse) + require.NoError(t, err) + appCodec := app.MakeEncodingConfig().Marshaler + + jsonMarshalledResponse, err := keeper.ConvertProtoToJSONMarshal(appCodec, tc.protoResponseStruct, originalVersionBz) + if tc.expectedError { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check response by json marshalling proto response into json response manually + jsonMarshalExpectedResponse, err := appCodec.MarshalJSON(tc.expectedProtoResponse) + require.NoError(t, err) + require.JSONEq(t, string(jsonMarshalledResponse), string(jsonMarshalExpectedResponse)) + }) + } +} + +// TestDeterministicJsonMarshal tests that we get deterministic JSON marshalled response upon +// proto struct update in the state machine. +func TestDeterministicJsonMarshal(t *testing.T) { + testCases := []struct { + name string + originalResponse string + updatedResponse string + queryPath string + responseProtoStruct codec.ProtoMarshaler + expectedProto func() codec.ProtoMarshaler + }{ + /** + * + * Origin Response + * 0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f7331346c3268686a6e676c3939367772703935673867646a6871653038326375367a7732706c686b + * + * Updated Response + * 0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f7331646a783375676866736d6b6135386676673076616a6e6533766c72776b7a6a346e6377747271122d636f736d6f7331646a783375676866736d6b6135386676673076616a6e6533766c72776b7a6a346e6377747271 + // Origin proto + message QueryAccountResponse { + // account defines the account of the corresponding address. + google.protobuf.Any account = 1 [(cosmos_proto.accepts_interface) = "AccountI"]; + } + // Updated proto + message QueryAccountResponse { + // account defines the account of the corresponding address. + google.protobuf.Any account = 1 [(cosmos_proto.accepts_interface) = "AccountI"]; + // address is the address to query for. + string address = 2; + } + */ + { + "Query Account", + "0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679", + "0a530a202f636f736d6f732e617574682e763162657461312e426173654163636f756e74122f0a2d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679122d636f736d6f733166387578756c746e3873717a687a6e72737a3371373778776171756867727367366a79766679", + "/cosmos.auth.v1beta1.Query/Account", + &authtypes.QueryAccountResponse{}, + func() codec.ProtoMarshaler { + account := authtypes.BaseAccount{ + Address: "cosmos1f8uxultn8sqzhznrsz3q77xwaquhgrsg6jyvfy", + } + accountResponse, err := codectypes.NewAnyWithValue(&account) + require.NoError(t, err) + return &authtypes.QueryAccountResponse{ + Account: accountResponse, + } + }, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("Case %s", tc.name), func(t *testing.T) { + appCodec := app.MakeEncodingConfig().Marshaler + + originVersionBz, err := hex.DecodeString(tc.originalResponse) + require.NoError(t, err) + jsonMarshalledOriginalBz, err := keeper.ConvertProtoToJSONMarshal(appCodec, tc.responseProtoStruct, originVersionBz) + require.NoError(t, err) + + newVersionBz, err := hex.DecodeString(tc.updatedResponse) + require.NoError(t, err) + jsonMarshalledUpdatedBz, err := keeper.ConvertProtoToJSONMarshal(appCodec, tc.responseProtoStruct, newVersionBz) + require.NoError(t, err) + + // json marshalled bytes should be the same since we use the same proto struct for unmarshalling + require.Equal(t, jsonMarshalledOriginalBz, jsonMarshalledUpdatedBz) + + // raw build also make same result + jsonMarshalExpectedResponse, err := appCodec.MarshalJSON(tc.expectedProto()) + require.NoError(t, err) + require.Equal(t, jsonMarshalledUpdatedBz, jsonMarshalExpectedResponse) + }) + } +} diff --git a/x/wasm/keeper/recurse_test.go b/x/wasm/keeper/recurse_test.go new file mode 100644 index 00000000..26712bf6 --- /dev/null +++ b/x/wasm/keeper/recurse_test.go @@ -0,0 +1,306 @@ +package keeper + +import ( + "encoding/json" + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cerc-io/laconicd/x/wasm/types" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Recurse struct { + Depth uint32 `json:"depth"` + Work uint32 `json:"work"` +} + +type recurseWrapper struct { + Recurse Recurse `json:"recurse"` +} + +func buildRecurseQuery(t *testing.T, msg Recurse) []byte { + wrapper := recurseWrapper{Recurse: msg} + bz, err := json.Marshal(wrapper) + require.NoError(t, err) + return bz +} + +type recurseResponse struct { + Hashed []byte `json:"hashed"` +} + +// number os wasm queries called from a contract +var totalWasmQueryCounter int + +func initRecurseContract(t *testing.T) (contract sdk.AccAddress, creator sdk.AccAddress, ctx sdk.Context, keeper *Keeper) { + countingQuerierDec := func(realWasmQuerier WasmVMQueryHandler) WasmVMQueryHandler { + return WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + totalWasmQueryCounter++ + return realWasmQuerier.HandleQuery(ctx, caller, request) + }) + } + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithQueryHandlerDecorator(countingQuerierDec)) + keeper = keepers.WasmKeeper + exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) + return exampleContract.Contract, exampleContract.CreatorAddr, ctx, keeper +} + +func TestGasCostOnQuery(t *testing.T) { + const ( + GasNoWork uint64 = 63_950 + // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256 + GasWork50 uint64 = 64_218 // this is a little shy of 50k gas - to keep an eye on the limit + + GasReturnUnhashed uint64 = 32 + GasReturnHashed uint64 = 27 + ) + + cases := map[string]struct { + gasLimit uint64 + msg Recurse + expectedGas uint64 + }{ + "no recursion, no work": { + gasLimit: 400_000, + msg: Recurse{}, + expectedGas: GasNoWork, + }, + "no recursion, some work": { + gasLimit: 400_000, + msg: Recurse{ + Work: 50, // 50 rounds of sha256 inside the contract + }, + expectedGas: GasWork50, + }, + "recursion 1, no work": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 1, + }, + expectedGas: 2*GasNoWork + GasReturnUnhashed, + }, + "recursion 1, some work": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 1, + Work: 50, + }, + expectedGas: 2*GasWork50 + GasReturnHashed, + }, + "recursion 4, some work": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 4, + Work: 50, + }, + expectedGas: 5*GasWork50 + 4*GasReturnHashed, + }, + } + + contractAddr, _, ctx, keeper := initRecurseContract(t) + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // external limit has no effect (we get a panic if this is enforced) + keeper.queryGasLimit = 1000 + + // make sure we set a limit before calling + ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // do the query + recurse := tc.msg + msg := buildRecurseQuery(t, recurse) + data, err := keeper.QuerySmart(ctx, contractAddr, msg) + require.NoError(t, err) + + // check the gas is what we expected + if types.EnableGasVerification { + assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed()) + } + // assert result is 32 byte sha256 hash (if hashed), or contractAddr if not + var resp recurseResponse + err = json.Unmarshal(data, &resp) + require.NoError(t, err) + if recurse.Work == 0 { + assert.Equal(t, len(contractAddr.String()), len(resp.Hashed)) + } else { + assert.Equal(t, 32, len(resp.Hashed)) + } + }) + } +} + +func TestGasOnExternalQuery(t *testing.T) { + const ( + GasWork50 uint64 = DefaultInstanceCost + 8_464 + ) + + cases := map[string]struct { + gasLimit uint64 + msg Recurse + expOutOfGas bool + }{ + "no recursion, plenty gas": { + gasLimit: 400_000, + msg: Recurse{ + Work: 50, // 50 rounds of sha256 inside the contract + }, + }, + "recursion 4, plenty gas": { + // this uses 244708 gas + gasLimit: 400_000, + msg: Recurse{ + Depth: 4, + Work: 50, + }, + }, + "no recursion, external gas limit": { + gasLimit: 5000, // this is not enough + msg: Recurse{ + Work: 50, + }, + expOutOfGas: true, + }, + "recursion 4, external gas limit": { + // this uses 244708 gas but give less + gasLimit: 4 * GasWork50, + msg: Recurse{ + Depth: 4, + Work: 50, + }, + expOutOfGas: true, + }, + } + + contractAddr, _, ctx, keeper := initRecurseContract(t) + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + recurse := tc.msg + msg := buildRecurseQuery(t, recurse) + + querier := NewGrpcQuerier(keeper.cdc, keeper.storeKey, keeper, tc.gasLimit) + req := &types.QuerySmartContractStateRequest{Address: contractAddr.String(), QueryData: msg} + _, gotErr := querier.SmartContractState(sdk.WrapSDKContext(ctx), req) + if tc.expOutOfGas { + require.Error(t, gotErr, sdkerrors.ErrOutOfGas) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestLimitRecursiveQueryGas(t *testing.T) { + // The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456 + // Basically, if I burn 90% of gas in CPU loop, then query out (to my self) + // the sub-query will have all the original gas (minus the 40k instance charge) + // and can burn 90% and call a sub-contract again... + // This attack would allow us to use far more than the provided gas before + // eventually hitting an OutOfGas panic. + + const ( + // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256 + GasWork2k uint64 = 77_206 // = NewContractInstanceCosts + x // we have 6x gas used in cpu than in the instance + // This is overhead for calling into a sub-contract + GasReturnHashed uint64 = 27 + ) + + cases := map[string]struct { + gasLimit uint64 + msg Recurse + expectQueriesFromContract int + expectedGas uint64 + expectOutOfGas bool + expectError string + }{ + "no recursion, lots of work": { + gasLimit: 4_000_000, + msg: Recurse{ + Depth: 0, + Work: 2000, + }, + expectQueriesFromContract: 0, + expectedGas: GasWork2k, + }, + "recursion 5, lots of work": { + gasLimit: 4_000_000, + msg: Recurse{ + Depth: 5, + Work: 2000, + }, + expectQueriesFromContract: 5, + // FIXME: why -1 ... confused a bit by calculations, seems like rounding issues + expectedGas: GasWork2k + 5*(GasWork2k+GasReturnHashed), + }, + // this is where we expect an error... + // it has enough gas to run 5 times and die on the 6th (5th time dispatching to sub-contract) + // however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times + "deep recursion, should die on 5th level": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 50, + Work: 2000, + }, + expectQueriesFromContract: 5, + expectOutOfGas: true, + }, + "very deep recursion, hits recursion limit": { + gasLimit: 10_000_000, + msg: Recurse{ + Depth: 100, + Work: 2000, + }, + expectQueriesFromContract: 10, + expectOutOfGas: false, + expectError: "query wasm contract failed", // Error we get from the contract instance doing the failing query, not wasmd + expectedGas: 10*(GasWork2k+GasReturnHashed) - 247, + }, + } + + contractAddr, _, ctx, keeper := initRecurseContract(t) + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // reset the counter before test + totalWasmQueryCounter = 0 + + // make sure we set a limit before calling + ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // prepare the query + recurse := tc.msg + msg := buildRecurseQuery(t, recurse) + + // if we expect out of gas, make sure this panics + if tc.expectOutOfGas { + require.Panics(t, func() { + _, err := keeper.QuerySmart(ctx, contractAddr, msg) + t.Logf("Got error not panic: %#v", err) + }) + assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter) + return + } + + // otherwise, we expect a successful call + _, err := keeper.QuerySmart(ctx, contractAddr, msg) + if tc.expectError != "" { + require.ErrorContains(t, err, tc.expectError) + } else { + require.NoError(t, err) + } + if types.EnableGasVerification { + assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed()) + } + assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter) + }) + } +} diff --git a/x/wasm/keeper/reflect_test.go b/x/wasm/keeper/reflect_test.go new file mode 100644 index 00000000..fa5f3b12 --- /dev/null +++ b/x/wasm/keeper/reflect_test.go @@ -0,0 +1,665 @@ +package keeper + +import ( + "encoding/json" + "os" + "strings" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/testdata" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// ReflectInitMsg is {} + +func buildReflectQuery(t *testing.T, query *testdata.ReflectQueryMsg) []byte { + bz, err := json.Marshal(query) + require.NoError(t, err) + return bz +} + +func mustParse(t *testing.T, data []byte, res interface{}) { + err := json.Unmarshal(data, res) + require.NoError(t, err) +} + +const ReflectFeatures = "staking,mask,stargate,cosmwasm_1_1" + +func TestReflectContractSend(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc))) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + _, _, bob := keyPubAddr() + + // upload reflect code + reflectID, _, err := keeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), reflectID) + + // upload hackatom escrow code + escrowCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + escrowID, _, err := keeper.Create(ctx, creator, escrowCode, nil) + require.NoError(t, err) + require.Equal(t, uint64(2), escrowID) + + // creator instantiates a contract and gives it tokens + reflectStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + reflectAddr, _, err := keeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), "reflect contract 2", reflectStart) + require.NoError(t, err) + require.NotEmpty(t, reflectAddr) + + // now we set contract as verifier of an escrow + initMsg := HackatomExampleInitMsg{ + Verifier: reflectAddr, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + escrowStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 25000)) + escrowAddr, _, err := keeper.Instantiate(ctx, escrowID, creator, nil, initMsgBz, "escrow contract 2", escrowStart) + require.NoError(t, err) + require.NotEmpty(t, escrowAddr) + + // let's make sure all balances make sense + checkAccount(t, ctx, accKeeper, bankKeeper, creator, sdk.NewCoins(sdk.NewInt64Coin("denom", 35000))) // 100k - 40k - 25k + checkAccount(t, ctx, accKeeper, bankKeeper, reflectAddr, reflectStart) + checkAccount(t, ctx, accKeeper, bankKeeper, escrowAddr, escrowStart) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, nil) + + // now for the trick.... we reflect a message through the reflect to call the escrow + // we also send an additional 14k tokens there. + // this should reduce the reflect balance by 14k (to 26k) + // this 14k is added to the escrow, then the entire balance is sent to bob (total: 39k) + approveMsg := []byte(`{"release":{}}`) + msgs := []wasmvmtypes.CosmosMsg{{ + Wasm: &wasmvmtypes.WasmMsg{ + Execute: &wasmvmtypes.ExecuteMsg{ + ContractAddr: escrowAddr.String(), + Msg: approveMsg, + Funds: []wasmvmtypes.Coin{{ + Denom: "denom", + Amount: "14000", + }}, + }, + }, + }} + reflectSend := testdata.ReflectHandleMsg{ + Reflect: &testdata.ReflectPayload{ + Msgs: msgs, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keeper.Execute(ctx, reflectAddr, creator, reflectSendBz, nil) + require.NoError(t, err) + + // did this work??? + checkAccount(t, ctx, accKeeper, bankKeeper, creator, sdk.NewCoins(sdk.NewInt64Coin("denom", 35000))) // same as before + checkAccount(t, ctx, accKeeper, bankKeeper, reflectAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 26000))) // 40k - 14k (from send) + checkAccount(t, ctx, accKeeper, bankKeeper, escrowAddr, sdk.Coins{}) // emptied reserved + checkAccount(t, ctx, accKeeper, bankKeeper, bob, sdk.NewCoins(sdk.NewInt64Coin("denom", 39000))) // all escrow of 25k + 14k +} + +func TestReflectCustomMsg(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + bob := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + _, _, fred := keyPubAddr() + + // upload code + codeID, _, err := keeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + contractAddr, _, err := keeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // set owner to bob + transfer := testdata.ReflectHandleMsg{ + ChangeOwner: &testdata.OwnerPayload{ + Owner: bob, + }, + } + transferBz, err := json.Marshal(transfer) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, creator, transferBz, nil) + require.NoError(t, err) + + // check some account values + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, contractStart) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, deposit) + checkAccount(t, ctx, accKeeper, bankKeeper, fred, nil) + + // bob can send contract's tokens to fred (using SendMsg) + msgs := []wasmvmtypes.CosmosMsg{{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: fred.String(), + Amount: []wasmvmtypes.Coin{{ + Denom: "denom", + Amount: "15000", + }}, + }, + }, + }} + reflectSend := testdata.ReflectHandleMsg{ + Reflect: &testdata.ReflectPayload{ + Msgs: msgs, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, reflectSendBz, nil) + require.NoError(t, err) + + // fred got coins + checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 15000))) + // contract lost them + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 25000))) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, deposit) + + // construct an opaque message + var sdkSendMsg sdk.Msg = &banktypes.MsgSend{ + FromAddress: contractAddr.String(), + ToAddress: fred.String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("denom", 23000)), + } + opaque, err := toReflectRawMsg(cdc, sdkSendMsg) + require.NoError(t, err) + reflectOpaque := testdata.ReflectHandleMsg{ + Reflect: &testdata.ReflectPayload{ + Msgs: []wasmvmtypes.CosmosMsg{opaque}, + }, + } + reflectOpaqueBz, err := json.Marshal(reflectOpaque) + require.NoError(t, err) + + _, err = keeper.Execute(ctx, contractAddr, bob, reflectOpaqueBz, nil) + require.NoError(t, err) + + // fred got more coins + checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 38000))) + // contract lost them + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 2000))) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, deposit) +} + +func TestMaskReflectCustomQuery(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // let's perform a normal query of state + ownerQuery := testdata.ReflectQueryMsg{ + Owner: &struct{}{}, + } + ownerQueryBz, err := json.Marshal(ownerQuery) + require.NoError(t, err) + ownerRes, err := keeper.QuerySmart(ctx, contractAddr, ownerQueryBz) + require.NoError(t, err) + var res testdata.OwnerResponse + err = json.Unmarshal(ownerRes, &res) + require.NoError(t, err) + assert.Equal(t, res.Owner, creator.String()) + + // and now making use of the custom querier callbacks + customQuery := testdata.ReflectQueryMsg{ + Capitalized: &testdata.Text{ + Text: "all Caps noW", + }, + } + customQueryBz, err := json.Marshal(customQuery) + require.NoError(t, err) + custom, err := keeper.QuerySmart(ctx, contractAddr, customQueryBz) + require.NoError(t, err) + var resp capitalizedResponse + err = json.Unmarshal(custom, &resp) + require.NoError(t, err) + assert.Equal(t, resp.Text, "ALL CAPS NOW") +} + +func TestReflectStargateQuery(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + keeper := keepers.WasmKeeper + + funds := sdk.NewCoins(sdk.NewInt64Coin("denom", 320000)) + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + expectedBalance := funds.Sub(contractStart) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, funds...) + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // first, normal query for the bank balance (to make sure our query is proper) + bankQuery := wasmvmtypes.QueryRequest{ + Bank: &wasmvmtypes.BankQuery{ + AllBalances: &wasmvmtypes.AllBalancesQuery{ + Address: creator.String(), + }, + }, + } + simpleQueryBz, err := json.Marshal(testdata.ReflectQueryMsg{ + Chain: &testdata.ChainQuery{Request: &bankQuery}, + }) + require.NoError(t, err) + simpleRes, err := keeper.QuerySmart(ctx, contractAddr, simpleQueryBz) + require.NoError(t, err) + var simpleChain testdata.ChainResponse + mustParse(t, simpleRes, &simpleChain) + var simpleBalance wasmvmtypes.AllBalancesResponse + mustParse(t, simpleChain.Data, &simpleBalance) + require.Equal(t, len(expectedBalance), len(simpleBalance.Amount)) + assert.Equal(t, simpleBalance.Amount[0].Amount, expectedBalance[0].Amount.String()) + assert.Equal(t, simpleBalance.Amount[0].Denom, expectedBalance[0].Denom) +} + +func TestReflectTotalSupplyQuery(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + keeper := keepers.WasmKeeper + // upload code + codeID := StoreReflectContract(t, ctx, keepers).CodeID + // creator instantiates a contract and gives it tokens + creator := RandomAccountAddress(t) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "testing", nil) + require.NoError(t, err) + + currentStakeSupply := keepers.BankKeeper.GetSupply(ctx, "stake") + require.NotEmpty(t, currentStakeSupply.Amount) // ensure we have real data + specs := map[string]struct { + denom string + expAmount wasmvmtypes.Coin + }{ + "known denom": { + denom: "stake", + expAmount: ConvertSdkCoinToWasmCoin(currentStakeSupply), + }, + "unknown denom": { + denom: "unknown", + expAmount: wasmvmtypes.Coin{Denom: "unknown", Amount: "0"}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + queryBz := mustMarshal(t, testdata.ReflectQueryMsg{ + Chain: &testdata.ChainQuery{ + Request: &wasmvmtypes.QueryRequest{ + Bank: &wasmvmtypes.BankQuery{ + Supply: &wasmvmtypes.SupplyQuery{spec.denom}, + }, + }, + }, + }) + simpleRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) + + // then + require.NoError(t, err) + var rsp testdata.ChainResponse + mustParse(t, simpleRes, &rsp) + var supplyRsp wasmvmtypes.SupplyResponse + mustParse(t, rsp.Data, &supplyRsp) + assert.Equal(t, spec.expAmount, supplyRsp.Amount, spec.expAmount) + }) + } +} + +func TestReflectInvalidStargateQuery(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + keeper := keepers.WasmKeeper + + funds := sdk.NewCoins(sdk.NewInt64Coin("denom", 320000)) + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, funds...) + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // now, try to build a protobuf query + protoQuery := banktypes.QueryAllBalancesRequest{ + Address: creator.String(), + } + protoQueryBin, err := proto.Marshal(&protoQuery) + protoRequest := wasmvmtypes.QueryRequest{ + Stargate: &wasmvmtypes.StargateQuery{ + Path: "/cosmos.bank.v1beta1.Query/AllBalances", + Data: protoQueryBin, + }, + } + protoQueryBz, err := json.Marshal(testdata.ReflectQueryMsg{ + Chain: &testdata.ChainQuery{Request: &protoRequest}, + }) + require.NoError(t, err) + + // make a query on the chain, should not be whitelisted + _, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz) + require.Error(t, err) + require.Contains(t, err.Error(), "Unsupported query") + + // now, try to build a protobuf query + protoRequest = wasmvmtypes.QueryRequest{ + Stargate: &wasmvmtypes.StargateQuery{ + Path: "/cosmos.tx.v1beta1.Service/GetTx", + Data: []byte{}, + }, + } + protoQueryBz, err = json.Marshal(testdata.ReflectQueryMsg{ + Chain: &testdata.ChainQuery{Request: &protoRequest}, + }) + require.NoError(t, err) + + // make a query on the chain, should be blacklisted + _, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz) + require.Error(t, err) + require.Contains(t, err.Error(), "Unsupported query") + + // and another one + protoRequest = wasmvmtypes.QueryRequest{ + Stargate: &wasmvmtypes.StargateQuery{ + Path: "/cosmos.base.tendermint.v1beta1.Service/GetNodeInfo", + Data: []byte{}, + }, + } + protoQueryBz, err = json.Marshal(testdata.ReflectQueryMsg{ + Chain: &testdata.ChainQuery{Request: &protoRequest}, + }) + require.NoError(t, err) + + // make a query on the chain, should be blacklisted + _, err = keeper.QuerySmart(ctx, contractAddr, protoQueryBz) + require.Error(t, err) + require.Contains(t, err.Error(), "Unsupported query") +} + +type reflectState struct { + Owner string `json:"owner"` +} + +func TestMaskReflectWasmQueries(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // upload reflect code + reflectID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), reflectID) + + // creator instantiates a contract and gives it tokens + reflectStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + reflectAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), "reflect contract 2", reflectStart) + require.NoError(t, err) + require.NotEmpty(t, reflectAddr) + + // for control, let's make some queries directly on the reflect + ownerQuery := buildReflectQuery(t, &testdata.ReflectQueryMsg{Owner: &struct{}{}}) + res, err := keeper.QuerySmart(ctx, reflectAddr, ownerQuery) + require.NoError(t, err) + var ownerRes testdata.OwnerResponse + mustParse(t, res, &ownerRes) + require.Equal(t, ownerRes.Owner, creator.String()) + + // and a raw query: cosmwasm_storage::Singleton uses 2 byte big-endian length-prefixed to store data + configKey := append([]byte{0, 6}, []byte("config")...) + raw := keeper.QueryRaw(ctx, reflectAddr, configKey) + var stateRes reflectState + mustParse(t, raw, &stateRes) + require.Equal(t, stateRes.Owner, creator.String()) + + // now, let's reflect a smart query into the x/wasm handlers and see if we get the same result + reflectOwnerQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Wasm: &wasmvmtypes.WasmQuery{ + Smart: &wasmvmtypes.SmartQuery{ + ContractAddr: reflectAddr.String(), + Msg: ownerQuery, + }, + }}}} + reflectOwnerBin := buildReflectQuery(t, &reflectOwnerQuery) + res, err = keeper.QuerySmart(ctx, reflectAddr, reflectOwnerBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + var reflectRes testdata.ChainResponse + mustParse(t, res, &reflectRes) + var reflectOwnerRes testdata.OwnerResponse + mustParse(t, reflectRes.Data, &reflectOwnerRes) + require.Equal(t, reflectOwnerRes.Owner, creator.String()) + + // and with queryRaw + reflectStateQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Wasm: &wasmvmtypes.WasmQuery{ + Raw: &wasmvmtypes.RawQuery{ + ContractAddr: reflectAddr.String(), + Key: configKey, + }, + }}}} + reflectStateBin := buildReflectQuery(t, &reflectStateQuery) + res, err = keeper.QuerySmart(ctx, reflectAddr, reflectStateBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + var reflectRawRes testdata.ChainResponse + mustParse(t, res, &reflectRawRes) + // now, with the raw data, we can parse it into state + var reflectStateRes reflectState + mustParse(t, reflectRawRes.Data, &reflectStateRes) + require.Equal(t, reflectStateRes.Owner, creator.String()) +} + +func TestWasmRawQueryWithNil(t *testing.T) { + cdc := MakeEncodingConfig(t).Marshaler + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageEncoders(reflectEncoders(cdc)), WithQueryPlugins(reflectPlugins())) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // upload reflect code + reflectID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), reflectID) + + // creator instantiates a contract and gives it tokens + reflectStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + reflectAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), "reflect contract 2", reflectStart) + require.NoError(t, err) + require.NotEmpty(t, reflectAddr) + + // control: query directly + missingKey := []byte{0, 1, 2, 3, 4} + raw := keeper.QueryRaw(ctx, reflectAddr, missingKey) + require.Nil(t, raw) + + // and with queryRaw + reflectQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Wasm: &wasmvmtypes.WasmQuery{ + Raw: &wasmvmtypes.RawQuery{ + ContractAddr: reflectAddr.String(), + Key: missingKey, + }, + }}}} + reflectStateBin := buildReflectQuery(t, &reflectQuery) + res, err := keeper.QuerySmart(ctx, reflectAddr, reflectStateBin) + require.NoError(t, err) + + // first we pull out the data from chain response, before parsing the original response + var reflectRawRes testdata.ChainResponse + mustParse(t, res, &reflectRawRes) + // and make sure there is no data + require.Empty(t, reflectRawRes.Data) + // we get an empty byte slice not nil (if anyone care in go-land) + require.Equal(t, []byte{}, reflectRawRes.Data) +} + +func checkAccount(t *testing.T, ctx sdk.Context, accKeeper authkeeper.AccountKeeper, bankKeeper bankkeeper.Keeper, addr sdk.AccAddress, expected sdk.Coins) { + acct := accKeeper.GetAccount(ctx, addr) + if expected == nil { + assert.Nil(t, acct) + } else { + assert.NotNil(t, acct) + if expected.Empty() { + // there is confusion between nil and empty slice... let's just treat them the same + assert.True(t, bankKeeper.GetAllBalances(ctx, acct.GetAddress()).Empty()) + } else { + assert.Equal(t, bankKeeper.GetAllBalances(ctx, acct.GetAddress()), expected) + } + } +} + +/**** Code to support custom messages *****/ + +type reflectCustomMsg struct { + Debug string `json:"debug,omitempty"` + Raw []byte `json:"raw,omitempty"` +} + +// toReflectRawMsg encodes an sdk msg using any type with json encoding. +// Then wraps it as an opaque message +func toReflectRawMsg(cdc codec.Codec, msg sdk.Msg) (wasmvmtypes.CosmosMsg, error) { + any, err := codectypes.NewAnyWithValue(msg) + if err != nil { + return wasmvmtypes.CosmosMsg{}, err + } + rawBz, err := cdc.MarshalJSON(any) + if err != nil { + return wasmvmtypes.CosmosMsg{}, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + customMsg, err := json.Marshal(reflectCustomMsg{ + Raw: rawBz, + }) + res := wasmvmtypes.CosmosMsg{ + Custom: customMsg, + } + return res, nil +} + +// reflectEncoders needs to be registered in test setup to handle custom message callbacks +func reflectEncoders(cdc codec.Codec) *MessageEncoders { + return &MessageEncoders{ + Custom: fromReflectRawMsg(cdc), + } +} + +// fromReflectRawMsg decodes msg.Data to an sdk.Msg using proto Any and json encoding. +// this needs to be registered on the Encoders +func fromReflectRawMsg(cdc codec.Codec) CustomEncoder { + return func(_sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) { + var custom reflectCustomMsg + err := json.Unmarshal(msg, &custom) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + if custom.Raw != nil { + var any codectypes.Any + if err := cdc.UnmarshalJSON(custom.Raw, &any); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + var msg sdk.Msg + if err := cdc.UnpackAny(&any, &msg); err != nil { + return nil, err + } + return []sdk.Msg{msg}, nil + } + if custom.Debug != "" { + return nil, sdkerrors.Wrapf(types.ErrInvalidMsg, "Custom Debug: %s", custom.Debug) + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown Custom message variant") + } +} + +type reflectCustomQuery struct { + Ping *struct{} `json:"ping,omitempty"` + Capitalized *testdata.Text `json:"capitalized,omitempty"` +} + +// this is from the go code back to the contract (capitalized or ping) +type customQueryResponse struct { + Msg string `json:"msg"` +} + +// these are the return values from contract -> go depending on type of query +type ownerResponse struct { + Owner string `json:"owner"` +} + +type capitalizedResponse struct { + Text string `json:"text"` +} + +type chainResponse struct { + Data []byte `json:"data"` +} + +// reflectPlugins needs to be registered in test setup to handle custom query callbacks +func reflectPlugins() *QueryPlugins { + return &QueryPlugins{ + Custom: performCustomQuery, + } +} + +func performCustomQuery(_ sdk.Context, request json.RawMessage) ([]byte, error) { + var custom reflectCustomQuery + err := json.Unmarshal(request, &custom) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + if custom.Capitalized != nil { + msg := strings.ToUpper(custom.Capitalized.Text) + return json.Marshal(customQueryResponse{Msg: msg}) + } + if custom.Ping != nil { + return json.Marshal(customQueryResponse{Msg: "pong"}) + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown Custom query variant") +} diff --git a/x/wasm/keeper/relay.go b/x/wasm/keeper/relay.go new file mode 100644 index 00000000..16488346 --- /dev/null +++ b/x/wasm/keeper/relay.go @@ -0,0 +1,203 @@ +package keeper + +import ( + "time" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ types.IBCContractKeeper = (*Keeper)(nil) + +// OnOpenChannel calls the contract to participate in the IBC channel handshake step. +// In the IBC protocol this is either the `Channel Open Init` event on the initiating chain or +// `Channel Open Try` on the counterparty chain. +// Protocol version and channel ordering should be verified for example. +// See https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#channel-lifecycle-management +func (k Keeper) OnOpenChannel( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCChannelOpenMsg, +) (string, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-open-channel") + _, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return "", err + } + + env := types.NewEnv(ctx, contractAddr) + querier := k.newQueryHandler(ctx, contractAddr) + + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.IBCChannelOpen(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return "", sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + if res != nil { + return res.Version, nil + } + return "", nil +} + +// OnConnectChannel calls the contract to let it know the IBC channel was established. +// In the IBC protocol this is either the `Channel Open Ack` event on the initiating chain or +// `Channel Open Confirm` on the counterparty chain. +// +// There is an open issue with the [cosmos-sdk](https://github.com/cosmos/cosmos-sdk/issues/8334) +// that the counterparty channelID is empty on the initiating chain +// See https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#channel-lifecycle-management +func (k Keeper) OnConnectChannel( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCChannelConnectMsg, +) error { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-connect-channel") + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return err + } + + env := types.NewEnv(ctx, contractAddr) + querier := k.newQueryHandler(ctx, contractAddr) + + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.IBCChannelConnect(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res) +} + +// OnCloseChannel calls the contract to let it know the IBC channel is closed. +// Calling modules MAY atomically execute appropriate application logic in conjunction with calling chanCloseConfirm. +// +// Once closed, channels cannot be reopened and identifiers cannot be reused. Identifier reuse is prevented because +// we want to prevent potential replay of previously sent packets +// See https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#channel-lifecycle-management +func (k Keeper) OnCloseChannel( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCChannelCloseMsg, +) error { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-close-channel") + + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return err + } + + params := types.NewEnv(ctx, contractAddr) + querier := k.newQueryHandler(ctx, contractAddr) + + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.IBCChannelClose(codeInfo.CodeHash, params, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res) +} + +// OnRecvPacket calls the contract to process the incoming IBC packet. The contract fully owns the data processing and +// returns the acknowledgement data for the chain level. This allows custom applications and protocols on top +// of IBC. Although it is recommended to use the standard acknowledgement envelope defined in +// https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#acknowledgement-envelope +// +// For more information see: https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#packet-flow--handling +func (k Keeper) OnRecvPacket( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCPacketReceiveMsg, +) ([]byte, error) { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-recv-packet") + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return nil, err + } + + env := types.NewEnv(ctx, contractAddr) + querier := k.newQueryHandler(ctx, contractAddr) + + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.IBCPacketReceive(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + if res.Err != "" { // handle error case as before https://github.com/CosmWasm/wasmvm/commit/c300106fe5c9426a495f8e10821e00a9330c56c6 + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, res.Err) + } + // note submessage reply results can overwrite the `Acknowledgement` data + return k.handleContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok.Messages, res.Ok.Attributes, res.Ok.Acknowledgement, res.Ok.Events) +} + +// OnAckPacket calls the contract to handle the "acknowledgement" data which can contain success or failure of a packet +// acknowledgement written on the receiving chain for example. This is application level data and fully owned by the +// contract. The use of the standard acknowledgement envelope is recommended: https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#acknowledgement-envelope +// +// On application errors the contract can revert an operation like returning tokens as in ibc-transfer. +// +// For more information see: https://github.com/cosmos/ics/tree/master/spec/ics-004-channel-and-packet-semantics#packet-flow--handling +func (k Keeper) OnAckPacket( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCPacketAckMsg, +) error { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-ack-packet") + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return err + } + + env := types.NewEnv(ctx, contractAddr) + querier := k.newQueryHandler(ctx, contractAddr) + + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.IBCPacketAck(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res) +} + +// OnTimeoutPacket calls the contract to let it know the packet was never received on the destination chain within +// the timeout boundaries. +// The contract should handle this on the application level and undo the original operation +func (k Keeper) OnTimeoutPacket( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCPacketTimeoutMsg, +) error { + defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-timeout-packet") + + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return err + } + + env := types.NewEnv(ctx, contractAddr) + querier := k.newQueryHandler(ctx, contractAddr) + + gas := k.runtimeGasForContract(ctx) + res, gasUsed, execErr := k.wasmVM.IBCPacketTimeout(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gas, costJSONDeserialization) + k.consumeRuntimeGas(ctx, gasUsed) + if execErr != nil { + return sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res) +} + +func (k Keeper) handleIBCBasicContractResponse(ctx sdk.Context, addr sdk.AccAddress, id string, res *wasmvmtypes.IBCBasicResponse) error { + _, err := k.handleContractResponse(ctx, addr, id, res.Messages, res.Attributes, nil, res.Events) + return err +} diff --git a/x/wasm/keeper/relay_test.go b/x/wasm/keeper/relay_test.go new file mode 100644 index 00000000..6de4b745 --- /dev/null +++ b/x/wasm/keeper/relay_test.go @@ -0,0 +1,703 @@ +package keeper + +import ( + "encoding/json" + "errors" + "math" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestOnOpenChannel(t *testing.T) { + var m wasmtesting.MockWasmer + wasmtesting.MakeIBCInstantiable(&m) + messenger := &wasmtesting.MockMessageHandler{} + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger)) + example := SeedNewContractInstance(t, parentCtx, keepers, &m) + const myContractGas = 40 + + specs := map[string]struct { + contractAddr sdk.AccAddress + contractGas sdk.Gas + contractErr error + expGas uint64 + expErr bool + }{ + "consume contract gas": { + contractAddr: example.Contract, + contractGas: myContractGas, + expGas: myContractGas, + }, + "consume max gas": { + contractAddr: example.Contract, + contractGas: math.MaxUint64 / DefaultGasMultiplier, + expGas: math.MaxUint64 / DefaultGasMultiplier, + }, + "consume gas on error": { + contractAddr: example.Contract, + contractGas: myContractGas, + contractErr: errors.New("test, ignore"), + expErr: true, + }, + "unknown contract address": { + contractAddr: RandomAccountAddress(t), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"} + myMsg := wasmvmtypes.IBCChannelOpenMsg{OpenTry: &wasmvmtypes.IBCOpenTry{Channel: myChannel, CounterpartyVersion: "foo"}} + m.IBCChannelOpenFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + assert.Equal(t, myMsg, msg) + return &wasmvmtypes.IBC3ChannelOpenResponse{}, spec.contractGas * DefaultGasMultiplier, spec.contractErr + } + + ctx, _ := parentCtx.CacheContext() + before := ctx.GasMeter().GasConsumed() + + // when + msg := wasmvmtypes.IBCChannelOpenMsg{ + OpenTry: &wasmvmtypes.IBCOpenTry{ + Channel: myChannel, + CounterpartyVersion: "foo", + }, + } + _, err := keepers.WasmKeeper.OnOpenChannel(ctx, spec.contractAddr, msg) + + // then + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + // verify gas consumed + const storageCosts = sdk.Gas(2903) + assert.Equal(t, spec.expGas, ctx.GasMeter().GasConsumed()-before-storageCosts) + }) + } +} + +func TestOnConnectChannel(t *testing.T) { + var m wasmtesting.MockWasmer + wasmtesting.MakeIBCInstantiable(&m) + messenger := &wasmtesting.MockMessageHandler{} + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger)) + example := SeedNewContractInstance(t, parentCtx, keepers, &m) + const myContractGas = 40 + + specs := map[string]struct { + contractAddr sdk.AccAddress + contractResp *wasmvmtypes.IBCBasicResponse + contractErr error + overwriteMessenger *wasmtesting.MockMessageHandler + expContractGas sdk.Gas + expErr bool + expEventTypes []string + }{ + "consume contract gas": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{}, + }, + "consume gas on error, ignore events + messages": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + contractErr: errors.New("test, ignore"), + expErr: true, + }, + "dispatch contract messages on success": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + }, + }, + "emit contract events on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "messenger errors returned, events stored": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + overwriteMessenger: wasmtesting.NewErroringMessageHandler(), + expErr: true, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "unknown contract address": { + contractAddr: RandomAccountAddress(t), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"} + myMsg := wasmvmtypes.IBCChannelConnectMsg{OpenConfirm: &wasmvmtypes.IBCOpenConfirm{Channel: myChannel}} + m.IBCChannelConnectFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + assert.Equal(t, msg, myMsg) + return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr + } + + ctx, _ := parentCtx.CacheContext() + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + before := ctx.GasMeter().GasConsumed() + msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler() + *messenger = *msger + if spec.overwriteMessenger != nil { + *messenger = *spec.overwriteMessenger + } + + // when + msg := wasmvmtypes.IBCChannelConnectMsg{ + OpenConfirm: &wasmvmtypes.IBCOpenConfirm{ + Channel: myChannel, + }, + } + err := keepers.WasmKeeper.OnConnectChannel(ctx, spec.contractAddr, msg) + + // then + if spec.expErr { + require.Error(t, err) + assert.Empty(t, capturedMsgs) // no messages captured on error + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + return + } + require.NoError(t, err) + // verify gas consumed + const storageCosts = sdk.Gas(2903) + assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts) + // verify msgs dispatched + require.Len(t, *capturedMsgs, len(spec.contractResp.Messages)) + for i, m := range spec.contractResp.Messages { + assert.Equal(t, (*capturedMsgs)[i], m.Msg) + } + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + }) + } +} + +func TestOnCloseChannel(t *testing.T) { + var m wasmtesting.MockWasmer + wasmtesting.MakeIBCInstantiable(&m) + messenger := &wasmtesting.MockMessageHandler{} + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger)) + example := SeedNewContractInstance(t, parentCtx, keepers, &m) + const myContractGas = 40 + + specs := map[string]struct { + contractAddr sdk.AccAddress + contractResp *wasmvmtypes.IBCBasicResponse + contractErr error + overwriteMessenger *wasmtesting.MockMessageHandler + expContractGas sdk.Gas + expErr bool + expEventTypes []string + }{ + "consume contract gas": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{}, + }, + "consume gas on error, ignore events + messages": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + contractErr: errors.New("test, ignore"), + expErr: true, + }, + "dispatch contract messages on success": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + }, + }, + "emit contract events on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "messenger errors returned, events stored": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + overwriteMessenger: wasmtesting.NewErroringMessageHandler(), + expErr: true, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "unknown contract address": { + contractAddr: RandomAccountAddress(t), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myChannel := wasmvmtypes.IBCChannel{Version: "my test channel"} + myMsg := wasmvmtypes.IBCChannelCloseMsg{CloseInit: &wasmvmtypes.IBCCloseInit{Channel: myChannel}} + m.IBCChannelCloseFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + assert.Equal(t, msg, myMsg) + return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr + } + + ctx, _ := parentCtx.CacheContext() + before := ctx.GasMeter().GasConsumed() + msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler() + *messenger = *msger + + if spec.overwriteMessenger != nil { + *messenger = *spec.overwriteMessenger + } + + // when + msg := wasmvmtypes.IBCChannelCloseMsg{ + CloseInit: &wasmvmtypes.IBCCloseInit{ + Channel: myChannel, + }, + } + err := keepers.WasmKeeper.OnCloseChannel(ctx, spec.contractAddr, msg) + + // then + if spec.expErr { + require.Error(t, err) + assert.Empty(t, capturedMsgs) // no messages captured on error + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + return + } + require.NoError(t, err) + // verify gas consumed + const storageCosts = sdk.Gas(2903) + assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts) + // verify msgs dispatched + require.Len(t, *capturedMsgs, len(spec.contractResp.Messages)) + for i, m := range spec.contractResp.Messages { + assert.Equal(t, (*capturedMsgs)[i], m.Msg) + } + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + }) + } +} + +func TestOnRecvPacket(t *testing.T) { + var m wasmtesting.MockWasmer + wasmtesting.MakeIBCInstantiable(&m) + messenger := &wasmtesting.MockMessageHandler{} + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger)) + example := SeedNewContractInstance(t, parentCtx, keepers, &m) + const myContractGas = 40 + const storageCosts = sdk.Gas(2903) + + specs := map[string]struct { + contractAddr sdk.AccAddress + contractResp *wasmvmtypes.IBCReceiveResponse + contractErr error + overwriteMessenger *wasmtesting.MockMessageHandler + mockReplyFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + expContractGas sdk.Gas + expAck []byte + expErr bool + expEventTypes []string + }{ + "consume contract gas": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + }, + expAck: []byte("myAck"), + }, + "can return empty ack": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCReceiveResponse{}, + }, + "consume gas on error, ignore events + messages": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + contractErr: errors.New("test, ignore"), + expErr: true, + }, + "dispatch contract messages on success": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + }, + expAck: []byte("myAck"), + }, + "emit contract attributes on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + expEventTypes: []string{types.WasmModuleEventType}, + expAck: []byte("myAck"), + }, + "emit contract events on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 46, // charge or custom event as well + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + Events: []wasmvmtypes.Event{{ + Type: "custom", + Attributes: []wasmvmtypes.EventAttribute{{ + Key: "message", + Value: "to rudi", + }}, + }}, + }, + expEventTypes: []string{types.WasmModuleEventType, "wasm-custom"}, + expAck: []byte("myAck"), + }, + "messenger errors returned, events stored": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + overwriteMessenger: wasmtesting.NewErroringMessageHandler(), + expErr: true, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "submessage reply can overwrite ack data": { + contractAddr: example.Contract, + expContractGas: myContractGas + storageCosts, + contractResp: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: []byte("myAck"), + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyAlways, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}}, + }, + mockReplyFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{Data: []byte("myBetterAck")}, 0, nil + }, + expAck: []byte("myBetterAck"), + expEventTypes: []string{types.EventTypeReply}, + }, + "unknown contract address": { + contractAddr: RandomAccountAddress(t), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myPacket := wasmvmtypes.IBCPacket{Data: []byte("my data")} + + m.IBCPacketReceiveFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + assert.Equal(t, myPacket, msg.Packet) + return &wasmvmtypes.IBCReceiveResult{Ok: spec.contractResp}, myContractGas * DefaultGasMultiplier, spec.contractErr + } + if spec.mockReplyFn != nil { + m.ReplyFn = spec.mockReplyFn + h, ok := keepers.WasmKeeper.wasmVMResponseHandler.(*DefaultWasmVMContractResponseHandler) + require.True(t, ok) + h.md = NewMessageDispatcher(messenger, keepers.WasmKeeper) + } + + ctx, _ := parentCtx.CacheContext() + before := ctx.GasMeter().GasConsumed() + + msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler() + *messenger = *msger + + if spec.overwriteMessenger != nil { + *messenger = *spec.overwriteMessenger + } + + // when + msg := wasmvmtypes.IBCPacketReceiveMsg{Packet: myPacket} + gotAck, err := keepers.WasmKeeper.OnRecvPacket(ctx, spec.contractAddr, msg) + + // then + if spec.expErr { + require.Error(t, err) + assert.Empty(t, capturedMsgs) // no messages captured on error + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + return + } + require.NoError(t, err) + require.Equal(t, spec.expAck, gotAck) + + // verify gas consumed + const storageCosts = sdk.Gas(2903) + assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts) + // verify msgs dispatched + require.Len(t, *capturedMsgs, len(spec.contractResp.Messages)) + for i, m := range spec.contractResp.Messages { + assert.Equal(t, (*capturedMsgs)[i], m.Msg) + } + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + }) + } +} + +func TestOnAckPacket(t *testing.T) { + var m wasmtesting.MockWasmer + wasmtesting.MakeIBCInstantiable(&m) + messenger := &wasmtesting.MockMessageHandler{} + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger)) + example := SeedNewContractInstance(t, parentCtx, keepers, &m) + const myContractGas = 40 + + specs := map[string]struct { + contractAddr sdk.AccAddress + contractResp *wasmvmtypes.IBCBasicResponse + contractErr error + overwriteMessenger *wasmtesting.MockMessageHandler + expContractGas sdk.Gas + expErr bool + expEventTypes []string + }{ + "consume contract gas": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{}, + }, + "consume gas on error, ignore events + messages": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + contractErr: errors.New("test, ignore"), + expErr: true, + }, + "dispatch contract messages on success": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + }, + }, + "emit contract events on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "messenger errors returned, events stored": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + overwriteMessenger: wasmtesting.NewErroringMessageHandler(), + expErr: true, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "unknown contract address": { + contractAddr: RandomAccountAddress(t), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myAck := wasmvmtypes.IBCPacketAckMsg{Acknowledgement: wasmvmtypes.IBCAcknowledgement{Data: []byte("myAck")}} + m.IBCPacketAckFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + assert.Equal(t, myAck, msg) + return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr + } + + ctx, _ := parentCtx.CacheContext() + before := ctx.GasMeter().GasConsumed() + msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler() + *messenger = *msger + + if spec.overwriteMessenger != nil { + *messenger = *spec.overwriteMessenger + } + + // when + err := keepers.WasmKeeper.OnAckPacket(ctx, spec.contractAddr, myAck) + + // then + + if spec.expErr { + require.Error(t, err) + assert.Empty(t, capturedMsgs) // no messages captured on error + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + return + } + require.NoError(t, err) + // verify gas consumed + const storageCosts = sdk.Gas(2903) + assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts) + // verify msgs dispatched + require.Len(t, *capturedMsgs, len(spec.contractResp.Messages)) + for i, m := range spec.contractResp.Messages { + assert.Equal(t, (*capturedMsgs)[i], m.Msg) + } + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + }) + } +} + +func TestOnTimeoutPacket(t *testing.T) { + var m wasmtesting.MockWasmer + wasmtesting.MakeIBCInstantiable(&m) + messenger := &wasmtesting.MockMessageHandler{} + parentCtx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithMessageHandler(messenger)) + example := SeedNewContractInstance(t, parentCtx, keepers, &m) + const myContractGas = 40 + + specs := map[string]struct { + contractAddr sdk.AccAddress + contractResp *wasmvmtypes.IBCBasicResponse + contractErr error + overwriteMessenger *wasmtesting.MockMessageHandler + expContractGas sdk.Gas + expErr bool + expEventTypes []string + }{ + "consume contract gas": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{}, + }, + "consume gas on error, ignore events + messages": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + contractErr: errors.New("test, ignore"), + expErr: true, + }, + "dispatch contract messages on success": { + contractAddr: example.Contract, + expContractGas: myContractGas, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + }, + }, + "emit contract attributes on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "emit contract events on success": { + contractAddr: example.Contract, + expContractGas: myContractGas + 46, // cost for custom events + contractResp: &wasmvmtypes.IBCBasicResponse{ + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + Events: []wasmvmtypes.Event{{ + Type: "custom", + Attributes: []wasmvmtypes.EventAttribute{{ + Key: "message", + Value: "to rudi", + }}, + }}, + }, + expEventTypes: []string{types.WasmModuleEventType, "wasm-custom"}, + }, + "messenger errors returned, events stored before": { + contractAddr: example.Contract, + expContractGas: myContractGas + 10, + contractResp: &wasmvmtypes.IBCBasicResponse{ + Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: &wasmvmtypes.BankMsg{}}}, {ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Custom: json.RawMessage(`{"foo":"bar"}`)}}}, + Attributes: []wasmvmtypes.EventAttribute{{Key: "Foo", Value: "Bar"}}, + }, + overwriteMessenger: wasmtesting.NewErroringMessageHandler(), + expErr: true, + expEventTypes: []string{types.WasmModuleEventType}, + }, + "unknown contract address": { + contractAddr: RandomAccountAddress(t), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + myPacket := wasmvmtypes.IBCPacket{Data: []byte("my test packet")} + m.IBCPacketTimeoutFn = func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + assert.Equal(t, myPacket, msg.Packet) + return spec.contractResp, myContractGas * DefaultGasMultiplier, spec.contractErr + } + + ctx, _ := parentCtx.CacheContext() + before := ctx.GasMeter().GasConsumed() + msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler() + *messenger = *msger + + if spec.overwriteMessenger != nil { + *messenger = *spec.overwriteMessenger + } + + // when + msg := wasmvmtypes.IBCPacketTimeoutMsg{Packet: myPacket} + err := keepers.WasmKeeper.OnTimeoutPacket(ctx, spec.contractAddr, msg) + + // then + if spec.expErr { + require.Error(t, err) + assert.Empty(t, capturedMsgs) // no messages captured on error + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + return + } + require.NoError(t, err) + // verify gas consumed + const storageCosts = sdk.Gas(2903) + assert.Equal(t, spec.expContractGas, ctx.GasMeter().GasConsumed()-before-storageCosts) + // verify msgs dispatched + require.Len(t, *capturedMsgs, len(spec.contractResp.Messages)) + for i, m := range spec.contractResp.Messages { + assert.Equal(t, (*capturedMsgs)[i], m.Msg) + } + assert.Equal(t, spec.expEventTypes, stripTypes(ctx.EventManager().Events())) + }) + } +} + +func stripTypes(events sdk.Events) []string { + var r []string + for _, e := range events { + r = append(r, e.Type) + } + return r +} diff --git a/x/wasm/keeper/snapshotter.go b/x/wasm/keeper/snapshotter.go new file mode 100644 index 00000000..d51b0570 --- /dev/null +++ b/x/wasm/keeper/snapshotter.go @@ -0,0 +1,156 @@ +package keeper + +import ( + "encoding/hex" + "io" + + snapshot "github.com/cosmos/cosmos-sdk/snapshots/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + protoio "github.com/gogo/protobuf/io" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cerc-io/laconicd/x/wasm/ioutils" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ snapshot.ExtensionSnapshotter = &WasmSnapshotter{} + +// SnapshotFormat format 1 is just gzipped wasm byte code for each item payload. No protobuf envelope, no metadata. +const SnapshotFormat = 1 + +type WasmSnapshotter struct { + wasm *Keeper + cms sdk.MultiStore +} + +func NewWasmSnapshotter(cms sdk.MultiStore, wasm *Keeper) *WasmSnapshotter { + return &WasmSnapshotter{ + wasm: wasm, + cms: cms, + } +} + +func (ws *WasmSnapshotter) SnapshotName() string { + return types.ModuleName +} + +func (ws *WasmSnapshotter) SnapshotFormat() uint32 { + return SnapshotFormat +} + +func (ws *WasmSnapshotter) SupportedFormats() []uint32 { + // If we support older formats, add them here and handle them in Restore + return []uint32{SnapshotFormat} +} + +func (ws *WasmSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { + cacheMS, err := ws.cms.CacheMultiStoreWithVersion(int64(height)) + if err != nil { + return err + } + + ctx := sdk.NewContext(cacheMS, tmproto.Header{}, false, log.NewNopLogger()) + seenBefore := make(map[string]bool) + var rerr error + + ws.wasm.IterateCodeInfos(ctx, func(id uint64, info types.CodeInfo) bool { + // Many code ids may point to the same code hash... only sync it once + hexHash := hex.EncodeToString(info.CodeHash) + // if seenBefore, just skip this one and move to the next + if seenBefore[hexHash] { + return false + } + seenBefore[hexHash] = true + + // load code and abort on error + wasmBytes, err := ws.wasm.GetByteCode(ctx, id) + if err != nil { + rerr = err + return true + } + + compressedWasm, err := ioutils.GzipIt(wasmBytes) + if err != nil { + rerr = err + return true + } + + err = snapshot.WriteExtensionItem(protoWriter, compressedWasm) + if err != nil { + rerr = err + return true + } + + return false + }) + + return rerr +} + +func (ws *WasmSnapshotter) Restore( + height uint64, format uint32, protoReader protoio.Reader, +) (snapshot.SnapshotItem, error) { + if format == SnapshotFormat { + return ws.processAllItems(height, protoReader, restoreV1, finalizeV1) + } + return snapshot.SnapshotItem{}, snapshot.ErrUnknownFormat +} + +func restoreV1(ctx sdk.Context, k *Keeper, compressedCode []byte) error { + if !ioutils.IsGzip(compressedCode) { + return types.ErrInvalid.Wrap("not a gzip") + } + wasmCode, err := ioutils.Uncompress(compressedCode, uint64(types.MaxWasmSize)) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + + // FIXME: check which codeIDs the checksum matches?? + _, err = k.wasmVM.Create(wasmCode) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + return nil +} + +func finalizeV1(ctx sdk.Context, k *Keeper) error { + // FIXME: ensure all codes have been uploaded? + return k.InitializePinnedCodes(ctx) +} + +func (ws *WasmSnapshotter) processAllItems( + height uint64, + protoReader protoio.Reader, + cb func(sdk.Context, *Keeper, []byte) error, + finalize func(sdk.Context, *Keeper) error, +) (snapshot.SnapshotItem, error) { + ctx := sdk.NewContext(ws.cms, tmproto.Header{Height: int64(height)}, false, log.NewNopLogger()) + + // keep the last item here... if we break, it will either be empty (if we hit io.EOF) + // or contain the last item (if we hit payload == nil) + var item snapshot.SnapshotItem + for { + item = snapshot.SnapshotItem{} + err := protoReader.ReadMsg(&item) + if err == io.EOF { + break + } else if err != nil { + return snapshot.SnapshotItem{}, sdkerrors.Wrap(err, "invalid protobuf message") + } + + // if it is not another ExtensionPayload message, then it is not for us. + // we should return it an let the manager handle this one + payload := item.GetExtensionPayload() + if payload == nil { + break + } + + if err := cb(ctx, ws.wasm, payload.Payload); err != nil { + return snapshot.SnapshotItem{}, sdkerrors.Wrap(err, "processing snapshot item") + } + } + + return item, finalize(ctx, ws.wasm) +} diff --git a/x/wasm/keeper/snapshotter_integration_test.go b/x/wasm/keeper/snapshotter_integration_test.go new file mode 100644 index 00000000..feb0b4ec --- /dev/null +++ b/x/wasm/keeper/snapshotter_integration_test.go @@ -0,0 +1,124 @@ +package keeper_test + +import ( + "crypto/sha256" + "os" + "testing" + "time" + + "github.com/cerc-io/laconicd/x/wasm/types" + + "github.com/stretchr/testify/assert" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cerc-io/laconicd/app" + "github.com/cerc-io/laconicd/x/wasm/keeper" +) + +func TestSnapshotter(t *testing.T) { + specs := map[string]struct { + wasmFiles []string + }{ + "single contract": { + wasmFiles: []string{"./testdata/reflect.wasm"}, + }, + "multiple contract": { + wasmFiles: []string{"./testdata/reflect.wasm", "./testdata/burner.wasm", "./testdata/reflect.wasm"}, + }, + "duplicate contracts": { + wasmFiles: []string{"./testdata/reflect.wasm", "./testdata/reflect.wasm"}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // setup source app + srcWasmApp, genesisAddr := newWasmExampleApp(t) + + // store wasm codes on chain + ctx := srcWasmApp.NewUncachedContext(false, tmproto.Header{ + ChainID: "foo", + Height: srcWasmApp.LastBlockHeight() + 1, + Time: time.Now(), + }) + wasmKeeper := app.NewTestSupport(t, srcWasmApp).WasmKeeper() + contractKeeper := keeper.NewDefaultPermissionKeeper(&wasmKeeper) + + srcCodeIDToChecksum := make(map[uint64][]byte, len(spec.wasmFiles)) + for i, v := range spec.wasmFiles { + wasmCode, err := os.ReadFile(v) + require.NoError(t, err) + codeID, checksum, err := contractKeeper.Create(ctx, genesisAddr, wasmCode, nil) + require.NoError(t, err) + require.Equal(t, uint64(i+1), codeID) + srcCodeIDToChecksum[codeID] = checksum + } + // create snapshot + srcWasmApp.Commit() + snapshotHeight := uint64(srcWasmApp.LastBlockHeight()) + snapshot, err := srcWasmApp.SnapshotManager().Create(snapshotHeight) + require.NoError(t, err) + assert.NotNil(t, snapshot) + + // when snapshot imported into dest app instance + destWasmApp := app.SetupWithEmptyStore(t) + require.NoError(t, destWasmApp.SnapshotManager().Restore(*snapshot)) + for i := uint32(0); i < snapshot.Chunks; i++ { + chunkBz, err := srcWasmApp.SnapshotManager().LoadChunk(snapshot.Height, snapshot.Format, i) + require.NoError(t, err) + end, err := destWasmApp.SnapshotManager().RestoreChunk(chunkBz) + require.NoError(t, err) + if end { + break + } + } + + // then all wasm contracts are imported + wasmKeeper = app.NewTestSupport(t, destWasmApp).WasmKeeper() + ctx = destWasmApp.NewUncachedContext(false, tmproto.Header{ + ChainID: "foo", + Height: destWasmApp.LastBlockHeight() + 1, + Time: time.Now(), + }) + + destCodeIDToChecksum := make(map[uint64][]byte, len(spec.wasmFiles)) + wasmKeeper.IterateCodeInfos(ctx, func(id uint64, info types.CodeInfo) bool { + bz, err := wasmKeeper.GetByteCode(ctx, id) + require.NoError(t, err) + hash := sha256.Sum256(bz) + destCodeIDToChecksum[id] = hash[:] + assert.Equal(t, hash[:], info.CodeHash) + return false + }) + assert.Equal(t, srcCodeIDToChecksum, destCodeIDToChecksum) + }) + } +} + +func newWasmExampleApp(t *testing.T) (*app.WasmApp, sdk.AccAddress) { + senderPrivKey := ed25519.GenPrivKey() + pubKey, err := cryptocodec.ToTmPubKeyInterface(senderPrivKey.PubKey()) + require.NoError(t, err) + + senderAddr := senderPrivKey.PubKey().Address().Bytes() + acc := authtypes.NewBaseAccount(senderAddr, senderPrivKey.PubKey(), 0, 0) + amount, ok := sdk.NewIntFromString("10000000000000000000") + require.True(t, ok) + + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)), + } + validator := tmtypes.NewValidator(pubKey, 1) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) + wasmApp := app.SetupWithGenesisValSet(t, valSet, []authtypes.GenesisAccount{acc}, "testing", nil, balance) + + return wasmApp, senderAddr +} diff --git a/x/wasm/keeper/staking_test.go b/x/wasm/keeper/staking_test.go new file mode 100644 index 00000000..679b8823 --- /dev/null +++ b/x/wasm/keeper/staking_test.go @@ -0,0 +1,748 @@ +package keeper + +import ( + "encoding/json" + "os" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/testdata" + wasmtypes "github.com/cerc-io/laconicd/x/wasm/types" +) + +type StakingInitMsg struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` + Validator sdk.ValAddress `json:"validator"` + ExitTax sdk.Dec `json:"exit_tax"` + // MinWithdrawal is uint128 encoded as a string (use sdk.Int?) + MinWithdrawl string `json:"min_withdrawal"` +} + +// StakingHandleMsg is used to encode handle messages +type StakingHandleMsg struct { + Transfer *transferPayload `json:"transfer,omitempty"` + Bond *struct{} `json:"bond,omitempty"` + Unbond *unbondPayload `json:"unbond,omitempty"` + Claim *struct{} `json:"claim,omitempty"` + Reinvest *struct{} `json:"reinvest,omitempty"` + Change *testdata.OwnerPayload `json:"change_owner,omitempty"` +} + +type transferPayload struct { + Recipient sdk.Address `json:"recipient"` + // uint128 encoded as string + Amount string `json:"amount"` +} + +type unbondPayload struct { + // uint128 encoded as string + Amount string `json:"amount"` +} + +// StakingQueryMsg is used to encode query messages +type StakingQueryMsg struct { + Balance *addressQuery `json:"balance,omitempty"` + Claims *addressQuery `json:"claims,omitempty"` + TokenInfo *struct{} `json:"token_info,omitempty"` + Investment *struct{} `json:"investment,omitempty"` +} + +type addressQuery struct { + Address sdk.AccAddress `json:"address"` +} + +type BalanceResponse struct { + Balance string `json:"balance,omitempty"` +} + +type ClaimsResponse struct { + Claims string `json:"claims,omitempty"` +} + +type TokenInfoResponse struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` +} + +type InvestmentResponse struct { + TokenSupply string `json:"token_supply"` + StakedTokens sdk.Coin `json:"staked_tokens"` + NominalValue sdk.Dec `json:"nominal_value"` + Owner sdk.AccAddress `json:"owner"` + Validator sdk.ValAddress `json:"validator"` + ExitTax sdk.Dec `json:"exit_tax"` + // MinWithdrawl is uint128 encoded as a string (use sdk.Int?) + MinWithdrawl string `json:"min_withdrawal"` +} + +func TestInitializeStaking(t *testing.T) { + ctx, k := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, stakingKeeper, keeper, bankKeeper := k.AccountKeeper, k.StakingKeeper, k.ContractKeeper, k.BankKeeper + + valAddr := addValidator(t, ctx, stakingKeeper, k.Faucet, sdk.NewInt64Coin("stake", 1234567)) + ctx = nextBlock(ctx, stakingKeeper) + v, found := stakingKeeper.GetValidator(ctx, valAddr) + assert.True(t, found) + assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1234567)) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000)) + creator := k.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // upload staking derivates code + stakingCode, err := os.ReadFile("./testdata/staking.wasm") + require.NoError(t, err) + stakingID, _, err := keeper.Create(ctx, creator, stakingCode, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), stakingID) + + // register to a valid address + initMsg := StakingInitMsg{ + Name: "Staking Derivatives", + Symbol: "DRV", + Decimals: 0, + Validator: valAddr, + ExitTax: sdk.MustNewDecFromStr("0.10"), + MinWithdrawl: "100", + } + initBz, err := json.Marshal(&initMsg) + require.NoError(t, err) + + stakingAddr, _, err := k.ContractKeeper.Instantiate(ctx, stakingID, creator, nil, initBz, "staking derivates - DRV", nil) + require.NoError(t, err) + require.NotEmpty(t, stakingAddr) + + // nothing spent here + checkAccount(t, ctx, accKeeper, bankKeeper, creator, deposit) + + // try to register with a validator not on the list and it fails + _, _, bob := keyPubAddr() + badInitMsg := StakingInitMsg{ + Name: "Missing Validator", + Symbol: "MISS", + Decimals: 0, + Validator: sdk.ValAddress(bob), + ExitTax: sdk.MustNewDecFromStr("0.10"), + MinWithdrawl: "100", + } + badBz, err := json.Marshal(&badInitMsg) + require.NoError(t, err) + + _, _, err = k.ContractKeeper.Instantiate(ctx, stakingID, creator, nil, badBz, "missing validator", nil) + require.Error(t, err) + + // no changes to bonding shares + val, _ := stakingKeeper.GetValidator(ctx, valAddr) + assert.Equal(t, val.GetDelegatorShares(), sdk.NewDec(1234567)) +} + +type initInfo struct { + valAddr sdk.ValAddress + creator sdk.AccAddress + contractAddr sdk.AccAddress + + ctx sdk.Context + accKeeper authkeeper.AccountKeeper + stakingKeeper stakingkeeper.Keeper + distKeeper distributionkeeper.Keeper + wasmKeeper Keeper + contractKeeper wasmtypes.ContractOpsKeeper + bankKeeper bankkeeper.Keeper + faucet *TestFaucet +} + +func initializeStaking(t *testing.T) initInfo { + ctx, k := CreateTestInput(t, false, AvailableCapabilities) + accKeeper, stakingKeeper, keeper, bankKeeper := k.AccountKeeper, k.StakingKeeper, k.WasmKeeper, k.BankKeeper + + valAddr := addValidator(t, ctx, stakingKeeper, k.Faucet, sdk.NewInt64Coin("stake", 1000000)) + ctx = nextBlock(ctx, stakingKeeper) + + // set some baseline - this seems to be needed + k.DistKeeper.SetValidatorHistoricalRewards(ctx, valAddr, 0, distributiontypes.ValidatorHistoricalRewards{ + CumulativeRewardRatio: sdk.DecCoins{}, + ReferenceCount: 1, + }) + + v, found := stakingKeeper.GetValidator(ctx, valAddr) + assert.True(t, found) + assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1000000)) + assert.Equal(t, v.Status, stakingtypes.Bonded) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000)) + creator := k.Faucet.NewFundedRandomAccount(ctx, deposit...) + + // upload staking derivates code + stakingCode, err := os.ReadFile("./testdata/staking.wasm") + require.NoError(t, err) + stakingID, _, err := k.ContractKeeper.Create(ctx, creator, stakingCode, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), stakingID) + + // register to a valid address + initMsg := StakingInitMsg{ + Name: "Staking Derivatives", + Symbol: "DRV", + Decimals: 0, + Validator: valAddr, + ExitTax: sdk.MustNewDecFromStr("0.10"), + MinWithdrawl: "100", + } + initBz, err := json.Marshal(&initMsg) + require.NoError(t, err) + + stakingAddr, _, err := k.ContractKeeper.Instantiate(ctx, stakingID, creator, nil, initBz, "staking derivates - DRV", nil) + require.NoError(t, err) + require.NotEmpty(t, stakingAddr) + + return initInfo{ + valAddr: valAddr, + creator: creator, + contractAddr: stakingAddr, + ctx: ctx, + accKeeper: accKeeper, + stakingKeeper: stakingKeeper, + wasmKeeper: *keeper, + distKeeper: k.DistKeeper, + bankKeeper: bankKeeper, + contractKeeper: k.ContractKeeper, + faucet: k.Faucet, + } +} + +func TestBonding(t *testing.T) { + initInfo := initializeStaking(t) + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper, bankKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper, initInfo.bankKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + initPower := val.GetDelegatorShares() + + // bob has 160k, putting 80k into the contract + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000)) + bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...) + + // check contract state before + assertBalance(t, ctx, keeper, contractAddr, bob, "0") + assertClaims(t, ctx, keeper, contractAddr, bob, "0") + assertSupply(t, ctx, keeper, contractAddr, "0", sdk.NewInt64Coin("stake", 0)) + + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // check some account values - the money is on neither account (cuz it is bonded) + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.Coins{}) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, funds) + + // make sure the proper number of tokens have been bonded + val, _ = stakingKeeper.GetValidator(ctx, valAddr) + finalPower := val.GetDelegatorShares() + assert.Equal(t, sdk.NewInt(80000), finalPower.Sub(initPower).TruncateInt()) + + // check the delegation itself + d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("80000")) + + // check we have the desired balance + assertBalance(t, ctx, keeper, contractAddr, bob, "80000") + assertClaims(t, ctx, keeper, contractAddr, bob, "0") + assertSupply(t, ctx, keeper, contractAddr, "80000", sdk.NewInt64Coin("stake", 80000)) +} + +func TestUnbonding(t *testing.T) { + initInfo := initializeStaking(t) + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper, bankKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper, initInfo.bankKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + initPower := val.GetDelegatorShares() + + // bob has 160k, putting 80k into the contract + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000)) + bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...) + + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit + ctx = nextBlock(ctx, stakingKeeper) + + // now unbond 30k - note that 3k (10%) goes to the owner as a tax, 27k unbonded and available as claims + unbond := StakingHandleMsg{ + Unbond: &unbondPayload{ + Amount: "30000", + }, + } + unbondBz, err := json.Marshal(unbond) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, unbondBz, nil) + require.NoError(t, err) + + // check some account values - the money is on neither account (cuz it is bonded) + // Note: why is this immediate? just test setup? + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.Coins{}) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, funds) + + // make sure the proper number of tokens have been bonded (80k - 27k = 53k) + val, _ = stakingKeeper.GetValidator(ctx, valAddr) + finalPower := val.GetDelegatorShares() + assert.Equal(t, sdk.NewInt(53000), finalPower.Sub(initPower).TruncateInt(), finalPower.String()) + + // check the delegation itself + d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("53000")) + + // check there is unbonding in progress + un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + require.Equal(t, 1, len(un.Entries)) + assert.Equal(t, "27000", un.Entries[0].Balance.String()) + + // check we have the desired balance + assertBalance(t, ctx, keeper, contractAddr, bob, "50000") + assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "3000") + assertClaims(t, ctx, keeper, contractAddr, bob, "27000") + assertSupply(t, ctx, keeper, contractAddr, "53000", sdk.NewInt64Coin("stake", 53000)) +} + +func TestReinvest(t *testing.T) { + initInfo := initializeStaking(t) + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper, bankKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper, initInfo.bankKeeper + distKeeper := initInfo.distKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + initPower := val.GetDelegatorShares() + assert.Equal(t, val.Tokens, sdk.NewInt(1000000), "%s", val.Tokens) + + // full is 2x funds, 1x goes to the contract, other stays on his wallet + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000)) + bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...) + + // we will stake 200k to a validator with 1M self-bond + // this means we should get 1/6 of the rewards + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit to solidify the delegation + ctx = nextBlock(ctx, stakingKeeper) + // we get 1/6, our share should be 40k minus 10% commission = 36k + setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000") + + // this should withdraw our outstanding 36k of rewards and reinvest them in the same delegation + reinvest := StakingHandleMsg{ + Reinvest: &struct{}{}, + } + reinvestBz, err := json.Marshal(reinvest) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, reinvestBz, nil) + require.NoError(t, err) + + // check some account values - the money is on neither account (cuz it is bonded) + // Note: why is this immediate? just test setup? + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.Coins{}) + checkAccount(t, ctx, accKeeper, bankKeeper, bob, funds) + + // check the delegation itself + d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + // we started with 200k and added 36k + assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("236000")) + + // make sure the proper number of tokens have been bonded (80k + 40k = 120k) + val, _ = stakingKeeper.GetValidator(ctx, valAddr) + finalPower := val.GetDelegatorShares() + assert.Equal(t, sdk.NewInt(236000), finalPower.Sub(initPower).TruncateInt(), finalPower.String()) + + // check there is no unbonding in progress + un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr) + assert.False(t, found, "%#v", un) + + // check we have the desired balance + assertBalance(t, ctx, keeper, contractAddr, bob, "200000") + assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "0") + assertClaims(t, ctx, keeper, contractAddr, bob, "0") + assertSupply(t, ctx, keeper, contractAddr, "200000", sdk.NewInt64Coin("stake", 236000)) +} + +func TestQueryStakingInfo(t *testing.T) { + // STEP 1: take a lot of setup from TestReinvest so we have non-zero info + initInfo := initializeStaking(t) + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper + distKeeper := initInfo.distKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + assert.Equal(t, sdk.NewInt(1000000), val.Tokens) + + // full is 2x funds, 1x goes to the contract, other stays on his wallet + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000)) + bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...) + + // we will stake 200k to a validator with 1M self-bond + // this means we should get 1/6 of the rewards + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit to solidify the delegation + ctx = nextBlock(ctx, stakingKeeper) + // we get 1/6, our share should be 40k minus 10% commission = 36k + setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000") + + // see what the current rewards are + origReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + + // STEP 2: Prepare the mask contract + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := initInfo.faucet.NewFundedRandomAccount(ctx, deposit...) + + // upload mask code + maskID, _, err := initInfo.contractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(2), maskID) + + // creator instantiates a contract and gives it tokens + maskAddr, _, err := initInfo.contractKeeper.Instantiate(ctx, maskID, creator, nil, []byte("{}"), "mask contract 2", nil) + require.NoError(t, err) + require.NotEmpty(t, maskAddr) + + // STEP 3: now, let's reflect some queries. + // let's get the bonded denom + reflectBondedQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{ + BondedDenom: &struct{}{}, + }}}} + reflectBondedBin := buildReflectQuery(t, &reflectBondedQuery) + res, err := keeper.QuerySmart(ctx, maskAddr, reflectBondedBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + var reflectRes testdata.ChainResponse + mustParse(t, res, &reflectRes) + var bondedRes wasmvmtypes.BondedDenomResponse + mustParse(t, reflectRes.Data, &bondedRes) + assert.Equal(t, "stake", bondedRes.Denom) + + // now, let's reflect a smart query into the x/wasm handlers and see if we get the same result + reflectAllValidatorsQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{ + AllValidators: &wasmvmtypes.AllValidatorsQuery{}, + }}}} + reflectAllValidatorsBin := buildReflectQuery(t, &reflectAllValidatorsQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectAllValidatorsBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var allValidatorsRes wasmvmtypes.AllValidatorsResponse + mustParse(t, reflectRes.Data, &allValidatorsRes) + require.Len(t, allValidatorsRes.Validators, 1) + valInfo := allValidatorsRes.Validators[0] + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), valInfo.Address) + require.Contains(t, valInfo.Commission, "0.100") + require.Contains(t, valInfo.MaxCommission, "0.200") + require.Contains(t, valInfo.MaxChangeRate, "0.010") + + // find a validator + reflectValidatorQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{ + Validator: &wasmvmtypes.ValidatorQuery{ + Address: valAddr.String(), + }, + }}}} + reflectValidatorBin := buildReflectQuery(t, &reflectValidatorQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectValidatorBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var validatorRes wasmvmtypes.ValidatorResponse + mustParse(t, reflectRes.Data, &validatorRes) + require.NotNil(t, validatorRes.Validator) + valInfo = *validatorRes.Validator + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), valInfo.Address) + require.Contains(t, valInfo.Commission, "0.100") + require.Contains(t, valInfo.MaxCommission, "0.200") + require.Contains(t, valInfo.MaxChangeRate, "0.010") + + // missing validator + noVal := sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()) + reflectNoValidatorQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{ + Validator: &wasmvmtypes.ValidatorQuery{ + Address: noVal.String(), + }, + }}}} + reflectNoValidatorBin := buildReflectQuery(t, &reflectNoValidatorQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectNoValidatorBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var noValidatorRes wasmvmtypes.ValidatorResponse + mustParse(t, reflectRes.Data, &noValidatorRes) + require.Nil(t, noValidatorRes.Validator) + + // test to get all my delegations + reflectAllDelegationsQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{ + AllDelegations: &wasmvmtypes.AllDelegationsQuery{ + Delegator: contractAddr.String(), + }, + }}}} + reflectAllDelegationsBin := buildReflectQuery(t, &reflectAllDelegationsQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectAllDelegationsBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var allDelegationsRes wasmvmtypes.AllDelegationsResponse + mustParse(t, reflectRes.Data, &allDelegationsRes) + require.Len(t, allDelegationsRes.Delegations, 1) + delInfo := allDelegationsRes.Delegations[0] + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), delInfo.Validator) + // note this is not bob (who staked to the contract), but the contract itself + require.Equal(t, contractAddr.String(), delInfo.Delegator) + // this is a different Coin type, with String not BigInt, compare field by field + require.Equal(t, funds[0].Denom, delInfo.Amount.Denom) + require.Equal(t, funds[0].Amount.String(), delInfo.Amount.Amount) + + // test to get one delegations + reflectDelegationQuery := testdata.ReflectQueryMsg{Chain: &testdata.ChainQuery{Request: &wasmvmtypes.QueryRequest{Staking: &wasmvmtypes.StakingQuery{ + Delegation: &wasmvmtypes.DelegationQuery{ + Validator: valAddr.String(), + Delegator: contractAddr.String(), + }, + }}}} + reflectDelegationBin := buildReflectQuery(t, &reflectDelegationQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectDelegationBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var delegationRes wasmvmtypes.DelegationResponse + mustParse(t, reflectRes.Data, &delegationRes) + assert.NotEmpty(t, delegationRes.Delegation) + delInfo2 := delegationRes.Delegation + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), delInfo2.Validator) + // note this is not bob (who staked to the contract), but the contract itself + require.Equal(t, contractAddr.String(), delInfo2.Delegator) + // this is a different Coin type, with String not BigInt, compare field by field + require.Equal(t, funds[0].Denom, delInfo2.Amount.Denom) + require.Equal(t, funds[0].Amount.String(), delInfo2.Amount.Amount) + + require.Equal(t, wasmvmtypes.NewCoin(200000, "stake"), delInfo2.CanRedelegate) + require.Len(t, delInfo2.AccumulatedRewards, 1) + // see bonding above to see how we calculate 36000 (240000 / 6 - 10% commission) + require.Equal(t, wasmvmtypes.NewCoin(36000, "stake"), delInfo2.AccumulatedRewards[0]) + + // ensure rewards did not change when querying (neither amount nor period) + finalReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + require.Equal(t, origReward, finalReward) +} + +func TestQueryStakingPlugin(t *testing.T) { + // STEP 1: take a lot of setup from TestReinvest so we have non-zero info + initInfo := initializeStaking(t) + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + stakingKeeper := initInfo.stakingKeeper + distKeeper := initInfo.distKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + assert.Equal(t, sdk.NewInt(1000000), val.Tokens) + + // full is 2x funds, 1x goes to the contract, other stays on his wallet + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000)) + bob := initInfo.faucet.NewFundedRandomAccount(ctx, full...) + + // we will stake 200k to a validator with 1M self-bond + // this means we should get 1/6 of the rewards + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = initInfo.contractKeeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit to solidify the delegation + ctx = nextBlock(ctx, stakingKeeper) + // we get 1/6, our share should be 40k minus 10% commission = 36k + setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000") + + // see what the current rewards are + origReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + + // Step 2: Try out the query plugins + query := wasmvmtypes.StakingQuery{ + Delegation: &wasmvmtypes.DelegationQuery{ + Delegator: contractAddr.String(), + Validator: valAddr.String(), + }, + } + raw, err := StakingQuerier(stakingKeeper, distKeeper)(ctx, &query) + require.NoError(t, err) + var res wasmvmtypes.DelegationResponse + mustParse(t, raw, &res) + assert.NotEmpty(t, res.Delegation) + delInfo := res.Delegation + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), delInfo.Validator) + // note this is not bob (who staked to the contract), but the contract itself + require.Equal(t, contractAddr.String(), delInfo.Delegator) + // this is a different Coin type, with String not BigInt, compare field by field + require.Equal(t, funds[0].Denom, delInfo.Amount.Denom) + require.Equal(t, funds[0].Amount.String(), delInfo.Amount.Amount) + + require.Equal(t, wasmvmtypes.NewCoin(200000, "stake"), delInfo.CanRedelegate) + require.Len(t, delInfo.AccumulatedRewards, 1) + // see bonding above to see how we calculate 36000 (240000 / 6 - 10% commission) + require.Equal(t, wasmvmtypes.NewCoin(36000, "stake"), delInfo.AccumulatedRewards[0]) + + // ensure rewards did not change when querying (neither amount nor period) + finalReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + require.Equal(t, origReward, finalReward) +} + +// adds a few validators and returns a list of validators that are registered +func addValidator(t *testing.T, ctx sdk.Context, stakingKeeper stakingkeeper.Keeper, faucet *TestFaucet, value sdk.Coin) sdk.ValAddress { + owner := faucet.NewFundedRandomAccount(ctx, value) + + privKey := secp256k1.GenPrivKey() + pubKey := privKey.PubKey() + addr := sdk.ValAddress(pubKey.Address()) + + pkAny, err := codectypes.NewAnyWithValue(pubKey) + require.NoError(t, err) + msg := stakingtypes.MsgCreateValidator{ + Description: types.Description{ + Moniker: "Validator power", + }, + Commission: types.CommissionRates{ + Rate: sdk.MustNewDecFromStr("0.1"), + MaxRate: sdk.MustNewDecFromStr("0.2"), + MaxChangeRate: sdk.MustNewDecFromStr("0.01"), + }, + MinSelfDelegation: sdk.OneInt(), + DelegatorAddress: owner.String(), + ValidatorAddress: addr.String(), + Pubkey: pkAny, + Value: value, + } + + h := staking.NewHandler(stakingKeeper) + _, err = h(ctx, &msg) + require.NoError(t, err) + return addr +} + +// this will commit the current set, update the block height and set historic info +// basically, letting two blocks pass +func nextBlock(ctx sdk.Context, stakingKeeper stakingkeeper.Keeper) sdk.Context { + staking.EndBlocker(ctx, stakingKeeper) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + staking.BeginBlocker(ctx, stakingKeeper) + return ctx +} + +func setValidatorRewards(ctx sdk.Context, stakingKeeper stakingkeeper.Keeper, distKeeper distributionkeeper.Keeper, valAddr sdk.ValAddress, reward string) { + // allocate some rewards + vali := stakingKeeper.Validator(ctx, valAddr) + amount, err := sdk.NewDecFromStr(reward) + if err != nil { + panic(err) + } + payout := sdk.DecCoins{{Denom: "stake", Amount: amount}} + distKeeper.AllocateTokensToValidator(ctx, vali, payout) +} + +func assertBalance(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) { + query := StakingQueryMsg{ + Balance: &addressQuery{ + Address: addr, + }, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + res, err := keeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var balance BalanceResponse + err = json.Unmarshal(res, &balance) + require.NoError(t, err) + assert.Equal(t, expected, balance.Balance) +} + +func assertClaims(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) { + query := StakingQueryMsg{ + Claims: &addressQuery{ + Address: addr, + }, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + res, err := keeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var claims ClaimsResponse + err = json.Unmarshal(res, &claims) + require.NoError(t, err) + assert.Equal(t, expected, claims.Claims) +} + +func assertSupply(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, expectedIssued string, expectedBonded sdk.Coin) { + query := StakingQueryMsg{Investment: &struct{}{}} + queryBz, err := json.Marshal(query) + require.NoError(t, err) + res, err := keeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var invest InvestmentResponse + err = json.Unmarshal(res, &invest) + require.NoError(t, err) + assert.Equal(t, expectedIssued, invest.TokenSupply) + assert.Equal(t, expectedBonded, invest.StakedTokens) +} diff --git a/x/wasm/keeper/submsg_test.go b/x/wasm/keeper/submsg_test.go new file mode 100644 index 00000000..fdb68820 --- /dev/null +++ b/x/wasm/keeper/submsg_test.go @@ -0,0 +1,552 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cerc-io/laconicd/x/wasm/keeper/testdata" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// test handing of submessages, very closely related to the reflect_test + +// Try a simple send, no gas limit to for a sanity check before trying table tests +func TestDispatchSubMsgSuccessCase(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, ReflectFeatures) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.WasmKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + creatorBalance := deposit.Sub(contractStart) + _, _, fred := keyPubAddr() + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // check some account values + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, contractStart) + checkAccount(t, ctx, accKeeper, bankKeeper, creator, creatorBalance) + checkAccount(t, ctx, accKeeper, bankKeeper, fred, nil) + + // creator can send contract's tokens to fred (using SendMsg) + msg := wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: fred.String(), + Amount: []wasmvmtypes.Coin{{ + Denom: "denom", + Amount: "15000", + }}, + }, + }, + } + reflectSend := testdata.ReflectHandleMsg{ + ReflectSubMsg: &testdata.ReflectSubPayload{ + Msgs: []wasmvmtypes.SubMsg{{ + ID: 7, + Msg: msg, + ReplyOn: wasmvmtypes.ReplyAlways, + }}, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil) + require.NoError(t, err) + + // fred got coins + checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 15000))) + // contract lost them + checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 25000))) + checkAccount(t, ctx, accKeeper, bankKeeper, creator, creatorBalance) + + // query the reflect state to ensure the result was stored + query := testdata.ReflectQueryMsg{ + SubMsgResult: &testdata.SubCall{ID: 7}, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) + require.NoError(t, err) + + var res wasmvmtypes.Reply + err = json.Unmarshal(queryRes, &res) + require.NoError(t, err) + assert.Equal(t, uint64(7), res.ID) + assert.Empty(t, res.Result.Err) + require.NotNil(t, res.Result.Ok) + sub := res.Result.Ok + assert.Empty(t, sub.Data) + // as of v0.28.0 we strip out all events that don't come from wasm contracts. can't trust the sdk. + require.Len(t, sub.Events, 0) +} + +func TestDispatchSubMsgErrorHandling(t *testing.T) { + fundedDenom := "funds" + fundedAmount := 1_000_000 + ctxGasLimit := uint64(1_000_000) + subGasLimit := uint64(300_000) + + // prep - create one chain and upload the code + ctx, keepers := CreateTestInput(t, false, ReflectFeatures) + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + ctx = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) + keeper := keepers.WasmKeeper + contractStart := sdk.NewCoins(sdk.NewInt64Coin(fundedDenom, int64(fundedAmount))) + uploader := keepers.Faucet.NewFundedRandomAccount(ctx, contractStart.Add(contractStart...)...) + + // upload code + reflectID, _, err := keepers.ContractKeeper.Create(ctx, uploader, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + + // create hackatom contract for testing (for infinite loop) + hackatomCode, err := os.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + hackatomID, _, err := keepers.ContractKeeper.Create(ctx, uploader, hackatomCode, nil) + require.NoError(t, err) + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + hackatomAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, hackatomID, uploader, nil, initMsgBz, "hackatom demo", contractStart) + require.NoError(t, err) + + validBankSend := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { + return wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: emptyAccount, + Amount: []wasmvmtypes.Coin{{ + Denom: fundedDenom, + Amount: strconv.Itoa(fundedAmount / 2), + }}, + }, + }, + } + } + + invalidBankSend := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { + return wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: emptyAccount, + Amount: []wasmvmtypes.Coin{{ + Denom: fundedDenom, + Amount: strconv.Itoa(fundedAmount * 2), + }}, + }, + }, + } + } + + infiniteLoop := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { + return wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Execute: &wasmvmtypes.ExecuteMsg{ + ContractAddr: hackatomAddr.String(), + Msg: []byte(`{"cpu_loop":{}}`), + }, + }, + } + } + + instantiateContract := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { + return wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Instantiate: &wasmvmtypes.InstantiateMsg{ + CodeID: reflectID, + Msg: []byte("{}"), + Label: "subcall reflect", + }, + }, + } + } + + type assertion func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) + + assertReturnedEvents := func(expectedEvents int) assertion { + return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { + require.Len(t, response.Ok.Events, expectedEvents) + } + } + + assertGasUsed := func(minGas, maxGas uint64) assertion { + return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { + gasUsed := ctx.GasMeter().GasConsumed() + assert.True(t, gasUsed >= minGas, "Used %d gas (less than expected %d)", gasUsed, minGas) + assert.True(t, gasUsed <= maxGas, "Used %d gas (more than expected %d)", gasUsed, maxGas) + } + } + + assertErrorString := func(shouldContain string) assertion { + return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { + assert.Contains(t, response.Err, shouldContain) + } + } + + assertGotContractAddr := func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { + // should get the events emitted on new contract + event := response.Ok.Events[0] + require.Equal(t, event.Type, "instantiate") + assert.Equal(t, event.Attributes[0].Key, "_contract_address") + eventAddr := event.Attributes[0].Value + assert.NotEqual(t, contract, eventAddr) + + var res types.MsgInstantiateContractResponse + keepers.EncodingConfig.Marshaler.MustUnmarshal(response.Ok.Data, &res) + assert.Equal(t, eventAddr, res.Address) + } + + cases := map[string]struct { + submsgID uint64 + // we will generate message from the + msg func(contract, emptyAccount string) wasmvmtypes.CosmosMsg + gasLimit *uint64 + + // true if we expect this to throw out of gas panic + isOutOfGasPanic bool + // true if we expect this execute to return an error (can be false when submessage errors) + executeError bool + // true if we expect submessage to return an error (but execute to return success) + subMsgError bool + // make assertions after dispatch + resultAssertions []assertion + }{ + "send tokens": { + submsgID: 5, + msg: validBankSend, + resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(95000, 96000)}, + }, + "not enough tokens": { + submsgID: 6, + msg: invalidBankSend, + subMsgError: true, + // uses less gas than the send tokens (cost of bank transfer) + resultAssertions: []assertion{assertGasUsed(76000, 79000), assertErrorString("codespace: sdk, code: 5")}, + }, + "out of gas panic with no gas limit": { + submsgID: 7, + msg: infiniteLoop, + isOutOfGasPanic: true, + }, + + "send tokens with limit": { + submsgID: 15, + msg: validBankSend, + gasLimit: &subGasLimit, + // uses same gas as call without limit (note we do not charge the 40k on reply) + resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(95000, 96000)}, + }, + "not enough tokens with limit": { + submsgID: 16, + msg: invalidBankSend, + subMsgError: true, + gasLimit: &subGasLimit, + // uses same gas as call without limit (note we do not charge the 40k on reply) + resultAssertions: []assertion{assertGasUsed(77800, 77900), assertErrorString("codespace: sdk, code: 5")}, + }, + "out of gas caught with gas limit": { + submsgID: 17, + msg: infiniteLoop, + subMsgError: true, + gasLimit: &subGasLimit, + // uses all the subGasLimit, plus the 52k or so for the main contract + resultAssertions: []assertion{assertGasUsed(subGasLimit+73000, subGasLimit+74000), assertErrorString("codespace: sdk, code: 11")}, + }, + "instantiate contract gets address in data and events": { + submsgID: 21, + msg: instantiateContract, + resultAssertions: []assertion{assertReturnedEvents(1), assertGotContractAddr}, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + creator := keepers.Faucet.NewFundedRandomAccount(ctx, contractStart...) + _, _, empty := keyPubAddr() + + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), fmt.Sprintf("contract %s", name), contractStart) + require.NoError(t, err) + + msg := tc.msg(contractAddr.String(), empty.String()) + reflectSend := testdata.ReflectHandleMsg{ + ReflectSubMsg: &testdata.ReflectSubPayload{ + Msgs: []wasmvmtypes.SubMsg{{ + ID: tc.submsgID, + Msg: msg, + GasLimit: tc.gasLimit, + ReplyOn: wasmvmtypes.ReplyAlways, + }}, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + + execCtx := ctx.WithGasMeter(sdk.NewGasMeter(ctxGasLimit)) + defer func() { + if tc.isOutOfGasPanic { + r := recover() + require.NotNil(t, r, "expected panic") + if _, ok := r.(sdk.ErrorOutOfGas); !ok { + t.Fatalf("Expected OutOfGas panic, got: %#v\n", r) + } + } + }() + _, err = keepers.ContractKeeper.Execute(execCtx, contractAddr, creator, reflectSendBz, nil) + + if tc.executeError { + require.Error(t, err) + } else { + require.NoError(t, err) + + // query the reply + query := testdata.ReflectQueryMsg{ + SubMsgResult: &testdata.SubCall{ID: tc.submsgID}, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) + require.NoError(t, err) + var res wasmvmtypes.Reply + err = json.Unmarshal(queryRes, &res) + require.NoError(t, err) + assert.Equal(t, tc.submsgID, res.ID) + + if tc.subMsgError { + require.NotEmpty(t, res.Result.Err) + require.Nil(t, res.Result.Ok) + } else { + require.Empty(t, res.Result.Err) + require.NotNil(t, res.Result.Ok) + } + + for _, assertion := range tc.resultAssertions { + assertion(t, execCtx, contractAddr.String(), empty.String(), res.Result) + } + + } + }) + } +} + +// Test an error case, where the Encoded doesn't return any sdk.Msg and we trigger(ed) a null pointer exception. +// This occurs with the IBC encoder. Test this. +func TestDispatchSubMsgEncodeToNoSdkMsg(t *testing.T) { + // fake out the bank handle to return success with no data + nilEncoder := func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]sdk.Msg, error) { + return nil, nil + } + customEncoders := &MessageEncoders{ + Bank: nilEncoder, + } + + ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageHandler(NewSDKMessageHandler(nil, customEncoders))) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + _, _, fred := keyPubAddr() + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + + // creator instantiates a contract and gives it tokens + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // creator can send contract's tokens to fred (using SendMsg) + msg := wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: fred.String(), + Amount: []wasmvmtypes.Coin{{ + Denom: "denom", + Amount: "15000", + }}, + }, + }, + } + reflectSend := testdata.ReflectHandleMsg{ + ReflectSubMsg: &testdata.ReflectSubPayload{ + Msgs: []wasmvmtypes.SubMsg{{ + ID: 7, + Msg: msg, + ReplyOn: wasmvmtypes.ReplyAlways, + }}, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil) + require.NoError(t, err) + + // query the reflect state to ensure the result was stored + query := testdata.ReflectQueryMsg{ + SubMsgResult: &testdata.SubCall{ID: 7}, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) + require.NoError(t, err) + + var res wasmvmtypes.Reply + err = json.Unmarshal(queryRes, &res) + require.NoError(t, err) + assert.Equal(t, uint64(7), res.ID) + assert.Empty(t, res.Result.Err) + require.NotNil(t, res.Result.Ok) + sub := res.Result.Ok + assert.Empty(t, sub.Data) + require.Len(t, sub.Events, 0) +} + +// Try a simple send, no gas limit to for a sanity check before trying table tests +func TestDispatchSubMsgConditionalReplyOn(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, ReflectFeatures) + keeper := keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + _, _, fred := keyPubAddr() + + // upload code + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) + require.NoError(t, err) + + // creator instantiates a contract and gives it tokens + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) + require.NoError(t, err) + + goodSend := wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: fred.String(), + Amount: []wasmvmtypes.Coin{{ + Denom: "denom", + Amount: "1000", + }}, + }, + }, + } + failSend := wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: fred.String(), + Amount: []wasmvmtypes.Coin{{ + Denom: "no-such-token", + Amount: "777777", + }}, + }, + }, + } + + cases := map[string]struct { + // true for wasmvmtypes.ReplySuccess, false for wasmvmtypes.ReplyError + replyOnSuccess bool + msg wasmvmtypes.CosmosMsg + // true if the call should return an error (it wasn't handled) + expectError bool + // true if the reflect contract wrote the response (success or error) - it was captured + writeResult bool + }{ + "all good, reply success": { + replyOnSuccess: true, + msg: goodSend, + expectError: false, + writeResult: true, + }, + "all good, reply error": { + replyOnSuccess: false, + msg: goodSend, + expectError: false, + writeResult: false, + }, + "bad msg, reply success": { + replyOnSuccess: true, + msg: failSend, + expectError: true, + writeResult: false, + }, + "bad msg, reply error": { + replyOnSuccess: false, + msg: failSend, + expectError: false, + writeResult: true, + }, + } + + var id uint64 = 0 + for name, tc := range cases { + id++ + t.Run(name, func(t *testing.T) { + subMsg := wasmvmtypes.SubMsg{ + ID: id, + Msg: tc.msg, + ReplyOn: wasmvmtypes.ReplySuccess, + } + if !tc.replyOnSuccess { + subMsg.ReplyOn = wasmvmtypes.ReplyError + } + + reflectSend := testdata.ReflectHandleMsg{ + ReflectSubMsg: &testdata.ReflectSubPayload{ + Msgs: []wasmvmtypes.SubMsg{subMsg}, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil) + + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + // query the reflect state to check if the result was stored + query := testdata.ReflectQueryMsg{ + SubMsgResult: &testdata.SubCall{ID: id}, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) + if tc.writeResult { + // we got some data for this call + require.NoError(t, err) + var res wasmvmtypes.Reply + err = json.Unmarshal(queryRes, &res) + require.NoError(t, err) + require.Equal(t, id, res.ID) + } else { + // nothing should be there -> error + require.Error(t, err) + } + }) + } +} diff --git a/x/wasm/keeper/test_common.go b/x/wasm/keeper/test_common.go new file mode 100644 index 00000000..2e329d76 --- /dev/null +++ b/x/wasm/keeper/test_common.go @@ -0,0 +1,759 @@ +package keeper + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/std" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/capability" + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/cosmos/cosmos-sdk/x/crisis" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + "github.com/cosmos/cosmos-sdk/x/distribution" + distrclient "github.com/cosmos/cosmos-sdk/x/distribution/client" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + "github.com/cosmos/cosmos-sdk/x/gov" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/mint" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/params" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + paramproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" + upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/cosmos/ibc-go/v4/modules/apps/transfer" + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + ibc "github.com/cosmos/ibc-go/v4/modules/core" + ibchost "github.com/cosmos/ibc-go/v4/modules/core/24-host" + ibckeeper "github.com/cosmos/ibc-go/v4/modules/core/keeper" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/rand" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" + + wasmappparams "github.com/cerc-io/laconicd/app/params" + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var moduleBasics = module.NewBasicManager( + auth.AppModuleBasic{}, + bank.AppModuleBasic{}, + capability.AppModuleBasic{}, + staking.AppModuleBasic{}, + mint.AppModuleBasic{}, + distribution.AppModuleBasic{}, + gov.NewAppModuleBasic( + paramsclient.ProposalHandler, distrclient.ProposalHandler, upgradeclient.ProposalHandler, + ), + params.AppModuleBasic{}, + crisis.AppModuleBasic{}, + slashing.AppModuleBasic{}, + ibc.AppModuleBasic{}, + upgrade.AppModuleBasic{}, + evidence.AppModuleBasic{}, + transfer.AppModuleBasic{}, + vesting.AppModuleBasic{}, +) + +func MakeTestCodec(t testing.TB) codec.Codec { + return MakeEncodingConfig(t).Marshaler +} + +func MakeEncodingConfig(_ testing.TB) wasmappparams.EncodingConfig { + encodingConfig := wasmappparams.MakeEncodingConfig() + amino := encodingConfig.Amino + interfaceRegistry := encodingConfig.InterfaceRegistry + + std.RegisterInterfaces(interfaceRegistry) + std.RegisterLegacyAminoCodec(amino) + + moduleBasics.RegisterLegacyAminoCodec(amino) + moduleBasics.RegisterInterfaces(interfaceRegistry) + // add wasmd types + types.RegisterInterfaces(interfaceRegistry) + types.RegisterLegacyAminoCodec(amino) + + return encodingConfig +} + +var TestingStakeParams = stakingtypes.Params{ + UnbondingTime: 100, + MaxValidators: 10, + MaxEntries: 10, + HistoricalEntries: 10, + BondDenom: "stake", +} + +type TestFaucet struct { + t testing.TB + bankKeeper bankkeeper.Keeper + sender sdk.AccAddress + balance sdk.Coins + minterModuleName string +} + +func NewTestFaucet(t testing.TB, ctx sdk.Context, bankKeeper bankkeeper.Keeper, minterModuleName string, initialAmount ...sdk.Coin) *TestFaucet { + require.NotEmpty(t, initialAmount) + r := &TestFaucet{t: t, bankKeeper: bankKeeper, minterModuleName: minterModuleName} + _, _, addr := keyPubAddr() + r.sender = addr + r.Mint(ctx, addr, initialAmount...) + r.balance = initialAmount + return r +} + +func (f *TestFaucet) Mint(parentCtx sdk.Context, addr sdk.AccAddress, amounts ...sdk.Coin) { + require.NotEmpty(f.t, amounts) + ctx := parentCtx.WithEventManager(sdk.NewEventManager()) // discard all faucet related events + err := f.bankKeeper.MintCoins(ctx, f.minterModuleName, amounts) + require.NoError(f.t, err) + err = f.bankKeeper.SendCoinsFromModuleToAccount(ctx, f.minterModuleName, addr, amounts) + require.NoError(f.t, err) + f.balance = f.balance.Add(amounts...) +} + +func (f *TestFaucet) Fund(parentCtx sdk.Context, receiver sdk.AccAddress, amounts ...sdk.Coin) { + require.NotEmpty(f.t, amounts) + // ensure faucet is always filled + if !f.balance.IsAllGTE(amounts) { + f.Mint(parentCtx, f.sender, amounts...) + } + ctx := parentCtx.WithEventManager(sdk.NewEventManager()) // discard all faucet related events + err := f.bankKeeper.SendCoins(ctx, f.sender, receiver, amounts) + require.NoError(f.t, err) + f.balance = f.balance.Sub(amounts) +} + +func (f *TestFaucet) NewFundedRandomAccount(ctx sdk.Context, amounts ...sdk.Coin) sdk.AccAddress { + _, _, addr := keyPubAddr() + f.Fund(ctx, addr, amounts...) + return addr +} + +type TestKeepers struct { + AccountKeeper authkeeper.AccountKeeper + StakingKeeper stakingkeeper.Keeper + DistKeeper distributionkeeper.Keeper + BankKeeper bankkeeper.Keeper + GovKeeper govkeeper.Keeper + ContractKeeper types.ContractOpsKeeper + WasmKeeper *Keeper + IBCKeeper *ibckeeper.Keeper + Router *baseapp.Router + EncodingConfig wasmappparams.EncodingConfig + Faucet *TestFaucet + MultiStore sdk.CommitMultiStore + ScopedWasmKeeper capabilitykeeper.ScopedKeeper +} + +// CreateDefaultTestInput common settings for CreateTestInput +func CreateDefaultTestInput(t testing.TB) (sdk.Context, TestKeepers) { + return CreateTestInput(t, false, "staking") +} + +// CreateTestInput 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, isCheckTx bool, availableCapabilities string, opts ...Option) (sdk.Context, TestKeepers) { + // Load default wasm config + return createTestInput(t, isCheckTx, availableCapabilities, types.DefaultWasmConfig(), dbm.NewMemDB(), opts...) +} + +// 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, + isCheckTx bool, + availableCapabilities string, + wasmConfig types.WasmConfig, + db dbm.DB, + opts ...Option, +) (sdk.Context, TestKeepers) { + tempDir := t.TempDir() + + keys := sdk.NewKVStoreKeys( + authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, + minttypes.StoreKey, distributiontypes.StoreKey, slashingtypes.StoreKey, + govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, + evidencetypes.StoreKey, ibctransfertypes.StoreKey, + capabilitytypes.StoreKey, feegrant.StoreKey, authzkeeper.StoreKey, + types.StoreKey, + ) + ms := store.NewCommitMultiStore(db) + for _, v := range keys { + ms.MountStoreWithDB(v, sdk.StoreTypeIAVL, db) + } + tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) + for _, v := range tkeys { + ms.MountStoreWithDB(v, sdk.StoreTypeTransient, db) + } + + memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) + for _, v := range memKeys { + ms.MountStoreWithDB(v, sdk.StoreTypeMemory, db) + } + + require.NoError(t, ms.LoadLatestVersion()) + + ctx := sdk.NewContext(ms, tmproto.Header{ + Height: 1234567, + Time: time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC), + }, isCheckTx, log.NewNopLogger()) + ctx = types.WithTXCounter(ctx, 0) + + encodingConfig := MakeEncodingConfig(t) + appCodec, legacyAmino := encodingConfig.Marshaler, encodingConfig.Amino + + paramsKeeper := paramskeeper.NewKeeper( + appCodec, + legacyAmino, + keys[paramstypes.StoreKey], + tkeys[paramstypes.TStoreKey], + ) + for _, m := range []string{ + authtypes.ModuleName, + banktypes.ModuleName, + stakingtypes.ModuleName, + minttypes.ModuleName, + distributiontypes.ModuleName, + slashingtypes.ModuleName, + crisistypes.ModuleName, + ibctransfertypes.ModuleName, + capabilitytypes.ModuleName, + ibchost.ModuleName, + govtypes.ModuleName, + types.ModuleName, + } { + paramsKeeper.Subspace(m) + } + subspace := func(m string) paramstypes.Subspace { + r, ok := paramsKeeper.GetSubspace(m) + require.True(t, ok) + return r + } + maccPerms := map[string][]string{ // module account permissions + authtypes.FeeCollectorName: nil, + distributiontypes.ModuleName: nil, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + govtypes.ModuleName: {authtypes.Burner}, + ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + types.ModuleName: {authtypes.Burner}, + } + accountKeeper := authkeeper.NewAccountKeeper( + appCodec, + keys[authtypes.StoreKey], // target store + subspace(authtypes.ModuleName), + authtypes.ProtoBaseAccount, // prototype + maccPerms, + ) + blockedAddrs := make(map[string]bool) + for acc := range maccPerms { + blockedAddrs[authtypes.NewModuleAddress(acc).String()] = true + } + + bankKeeper := bankkeeper.NewBaseKeeper( + appCodec, + keys[banktypes.StoreKey], + accountKeeper, + subspace(banktypes.ModuleName), + blockedAddrs, + ) + bankKeeper.SetParams(ctx, banktypes.DefaultParams()) + + stakingKeeper := stakingkeeper.NewKeeper( + appCodec, + keys[stakingtypes.StoreKey], + accountKeeper, + bankKeeper, + subspace(stakingtypes.ModuleName), + ) + stakingKeeper.SetParams(ctx, TestingStakeParams) + + distKeeper := distributionkeeper.NewKeeper( + appCodec, + keys[distributiontypes.StoreKey], + subspace(distributiontypes.ModuleName), + accountKeeper, + bankKeeper, + stakingKeeper, + authtypes.FeeCollectorName, + nil, + ) + distKeeper.SetParams(ctx, distributiontypes.DefaultParams()) + stakingKeeper.SetHooks(distKeeper.Hooks()) + + // set genesis items required for distribution + distKeeper.SetFeePool(ctx, distributiontypes.InitialFeePool()) + + upgradeKeeper := upgradekeeper.NewKeeper( + map[int64]bool{}, + keys[upgradetypes.StoreKey], + appCodec, + tempDir, + nil, + ) + + faucet := NewTestFaucet(t, ctx, bankKeeper, minttypes.ModuleName, sdk.NewCoin("stake", sdk.NewInt(100_000_000_000))) + + // set some funds ot pay out validatores, based on code from: + // https://github.com/cosmos/cosmos-sdk/blob/fea231556aee4d549d7551a6190389c4328194eb/x/distribution/keeper/keeper_test.go#L50-L57 + distrAcc := distKeeper.GetDistributionAccount(ctx) + faucet.Fund(ctx, distrAcc.GetAddress(), sdk.NewCoin("stake", sdk.NewInt(2000000))) + accountKeeper.SetModuleAccount(ctx, distrAcc) + + capabilityKeeper := capabilitykeeper.NewKeeper( + appCodec, + keys[capabilitytypes.StoreKey], + memKeys[capabilitytypes.MemStoreKey], + ) + scopedIBCKeeper := capabilityKeeper.ScopeToModule(ibchost.ModuleName) + scopedWasmKeeper := capabilityKeeper.ScopeToModule(types.ModuleName) + + ibcKeeper := ibckeeper.NewKeeper( + appCodec, + keys[ibchost.StoreKey], + subspace(ibchost.ModuleName), + stakingKeeper, + upgradeKeeper, + scopedIBCKeeper, + ) + + router := baseapp.NewRouter() + bh := bank.NewHandler(bankKeeper) + router.AddRoute(sdk.NewRoute(banktypes.RouterKey, bh)) + sh := staking.NewHandler(stakingKeeper) + router.AddRoute(sdk.NewRoute(stakingtypes.RouterKey, sh)) + dh := distribution.NewHandler(distKeeper) + router.AddRoute(sdk.NewRoute(distributiontypes.RouterKey, dh)) + + querier := baseapp.NewGRPCQueryRouter() + querier.SetInterfaceRegistry(encodingConfig.InterfaceRegistry) + msgRouter := baseapp.NewMsgServiceRouter() + msgRouter.SetInterfaceRegistry(encodingConfig.InterfaceRegistry) + + cfg := sdk.GetConfig() + cfg.SetAddressVerifier(types.VerifyAddressLen()) + + keeper := NewKeeper( + appCodec, + keys[types.StoreKey], + subspace(types.ModuleName), + accountKeeper, + bankKeeper, + stakingKeeper, + distKeeper, + ibcKeeper.ChannelKeeper, + &ibcKeeper.PortKeeper, + scopedWasmKeeper, + wasmtesting.MockIBCTransferKeeper{}, + msgRouter, + querier, + tempDir, + wasmConfig, + availableCapabilities, + opts..., + ) + keeper.SetParams(ctx, types.DefaultParams()) + // add wasm handler so we can loop-back (contracts calling contracts) + contractKeeper := NewDefaultPermissionKeeper(&keeper) + router.AddRoute(sdk.NewRoute(types.RouterKey, TestHandler(contractKeeper))) + + am := module.NewManager( // minimal module set that we use for message/ query tests + bank.NewAppModule(appCodec, bankKeeper, accountKeeper), + staking.NewAppModule(appCodec, stakingKeeper, accountKeeper, bankKeeper), + distribution.NewAppModule(appCodec, distKeeper, accountKeeper, bankKeeper, stakingKeeper), + ) + am.RegisterServices(module.NewConfigurator(appCodec, msgRouter, querier)) + types.RegisterMsgServer(msgRouter, NewMsgServerImpl(NewDefaultPermissionKeeper(keeper))) + types.RegisterQueryServer(querier, NewGrpcQuerier(appCodec, keys[types.ModuleName], keeper, keeper.queryGasLimit)) + + govRouter := govtypes.NewRouter(). + AddRoute(govtypes.RouterKey, govtypes.ProposalHandler). + AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(paramsKeeper)). + AddRoute(distributiontypes.RouterKey, distribution.NewCommunityPoolSpendProposalHandler(distKeeper)). + AddRoute(types.RouterKey, NewWasmProposalHandler(&keeper, types.EnableAllProposals)) + + govKeeper := govkeeper.NewKeeper( + appCodec, + keys[govtypes.StoreKey], + subspace(govtypes.ModuleName).WithKeyTable(govtypes.ParamKeyTable()), + accountKeeper, + bankKeeper, + stakingKeeper, + govRouter, + ) + + govKeeper.SetProposalID(ctx, govtypes.DefaultStartingProposalID) + govKeeper.SetDepositParams(ctx, govtypes.DefaultDepositParams()) + govKeeper.SetVotingParams(ctx, govtypes.DefaultVotingParams()) + govKeeper.SetTallyParams(ctx, govtypes.DefaultTallyParams()) + + keepers := TestKeepers{ + AccountKeeper: accountKeeper, + StakingKeeper: stakingKeeper, + DistKeeper: distKeeper, + ContractKeeper: contractKeeper, + WasmKeeper: &keeper, + BankKeeper: bankKeeper, + GovKeeper: govKeeper, + IBCKeeper: ibcKeeper, + Router: router, + EncodingConfig: encodingConfig, + Faucet: faucet, + MultiStore: ms, + ScopedWasmKeeper: scopedWasmKeeper, + } + return ctx, keepers +} + +// TestHandler returns a wasm handler for tests (to avoid circular imports) +func TestHandler(k types.ContractOpsKeeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + switch msg := msg.(type) { + case *types.MsgStoreCode: + return handleStoreCode(ctx, k, msg) + case *types.MsgInstantiateContract: + return handleInstantiate(ctx, k, msg) + case *types.MsgExecuteContract: + return handleExecute(ctx, k, msg) + default: + errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg) + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg) + } + } +} + +func handleStoreCode(ctx sdk.Context, k types.ContractOpsKeeper, msg *types.MsgStoreCode) (*sdk.Result, error) { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + codeID, _, err := k.Create(ctx, senderAddr, msg.WASMByteCode, msg.InstantiatePermission) + if err != nil { + return nil, err + } + + return &sdk.Result{ + Data: []byte(fmt.Sprintf("%d", codeID)), + Events: ctx.EventManager().ABCIEvents(), + }, nil +} + +func handleInstantiate(ctx sdk.Context, k types.ContractOpsKeeper, msg *types.MsgInstantiateContract) (*sdk.Result, error) { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + var adminAddr sdk.AccAddress + if msg.Admin != "" { + if adminAddr, err = sdk.AccAddressFromBech32(msg.Admin); err != nil { + return nil, sdkerrors.Wrap(err, "admin") + } + } + + contractAddr, _, err := k.Instantiate(ctx, msg.CodeID, senderAddr, adminAddr, msg.Msg, msg.Label, msg.Funds) + if err != nil { + return nil, err + } + + return &sdk.Result{ + Data: contractAddr, + Events: ctx.EventManager().Events().ToABCIEvents(), + }, nil +} + +func handleExecute(ctx sdk.Context, k types.ContractOpsKeeper, msg *types.MsgExecuteContract) (*sdk.Result, error) { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "sender") + } + contractAddr, err := sdk.AccAddressFromBech32(msg.Contract) + if err != nil { + return nil, sdkerrors.Wrap(err, "admin") + } + data, err := k.Execute(ctx, contractAddr, senderAddr, msg.Msg, msg.Funds) + if err != nil { + return nil, err + } + + return &sdk.Result{ + Data: data, + Events: ctx.EventManager().Events().ToABCIEvents(), + }, nil +} + +func RandomAccountAddress(_ testing.TB) sdk.AccAddress { + _, _, addr := keyPubAddr() + return addr +} + +// DeterministicAccountAddress creates a test address with v repeated to valid address size +func DeterministicAccountAddress(_ testing.TB, v byte) sdk.AccAddress { + return bytes.Repeat([]byte{v}, address.Len) +} + +func RandomBech32AccountAddress(t testing.TB) string { + return RandomAccountAddress(t).String() +} + +type ExampleContract struct { + InitialAmount sdk.Coins + Creator crypto.PrivKey + CreatorAddr sdk.AccAddress + CodeID uint64 + Checksum []byte +} + +func StoreHackatomExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract { + return StoreExampleContract(t, ctx, keepers, "./testdata/hackatom.wasm") +} + +func StoreBurnerExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract { + return StoreExampleContract(t, ctx, keepers, "./testdata/burner.wasm") +} + +func StoreIBCReflectContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract { + return StoreExampleContract(t, ctx, keepers, "./testdata/ibc_reflect.wasm") +} + +func StoreReflectContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleContract { + return StoreExampleContract(t, ctx, keepers, "./testdata/reflect.wasm") +} + +func StoreExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers, wasmFile string) ExampleContract { + anyAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000)) + creator, _, creatorAddr := keyPubAddr() + fundAccounts(t, ctx, keepers.AccountKeeper, keepers.BankKeeper, creatorAddr, anyAmount) + + wasmCode, err := os.ReadFile(wasmFile) + require.NoError(t, err) + + codeID, _, err := keepers.ContractKeeper.Create(ctx, creatorAddr, wasmCode, nil) + require.NoError(t, err) + hash := keepers.WasmKeeper.GetCodeInfo(ctx, codeID).CodeHash + return ExampleContract{anyAmount, creator, creatorAddr, codeID, hash} +} + +var wasmIdent = []byte("\x00\x61\x73\x6D") + +type ExampleContractInstance struct { + ExampleContract + Contract sdk.AccAddress +} + +// SeedNewContractInstance sets the mock wasmerEngine in keeper and calls store + instantiate to init the contract's metadata +func SeedNewContractInstance(t testing.TB, ctx sdk.Context, keepers TestKeepers, mock types.WasmerEngine) ExampleContractInstance { + t.Helper() + exampleContract := StoreRandomContract(t, ctx, keepers, mock) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, exampleContract.CodeID, exampleContract.CreatorAddr, exampleContract.CreatorAddr, []byte(`{}`), "", nil) + require.NoError(t, err) + return ExampleContractInstance{ + ExampleContract: exampleContract, + Contract: contractAddr, + } +} + +// StoreRandomContract sets the mock wasmerEngine in keeper and calls store +func StoreRandomContract(t testing.TB, ctx sdk.Context, keepers TestKeepers, mock types.WasmerEngine) ExampleContract { + return StoreRandomContractWithAccessConfig(t, ctx, keepers, mock, nil) +} + +func StoreRandomContractWithAccessConfig( + t testing.TB, ctx sdk.Context, + keepers TestKeepers, + mock types.WasmerEngine, + cfg *types.AccessConfig, +) ExampleContract { + t.Helper() + anyAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000)) + creator, _, creatorAddr := keyPubAddr() + fundAccounts(t, ctx, keepers.AccountKeeper, keepers.BankKeeper, creatorAddr, anyAmount) + keepers.WasmKeeper.wasmVM = mock + wasmCode := append(wasmIdent, rand.Bytes(10)...) //nolint:gocritic + codeID, checksum, err := keepers.ContractKeeper.Create(ctx, creatorAddr, wasmCode, cfg) + require.NoError(t, err) + exampleContract := ExampleContract{InitialAmount: anyAmount, Creator: creator, CreatorAddr: creatorAddr, CodeID: codeID, Checksum: checksum} + return exampleContract +} + +type HackatomExampleInstance struct { + ExampleContract + Contract sdk.AccAddress + Verifier crypto.PrivKey + VerifierAddr sdk.AccAddress + Beneficiary crypto.PrivKey + BeneficiaryAddr sdk.AccAddress + Label string + Deposit sdk.Coins +} + +// InstantiateHackatomExampleContract load and instantiate the "./testdata/hackatom.wasm" contract +func InstantiateHackatomExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) HackatomExampleInstance { + contract := StoreHackatomExampleContract(t, ctx, keepers) + + verifier, _, verifierAddr := keyPubAddr() + fundAccounts(t, ctx, keepers.AccountKeeper, keepers.BankKeeper, verifierAddr, contract.InitialAmount) + + beneficiary, _, beneficiaryAddr := keyPubAddr() + initMsgBz := HackatomExampleInitMsg{ + Verifier: verifierAddr, + Beneficiary: beneficiaryAddr, + }.GetBytes(t) + initialAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + + adminAddr := contract.CreatorAddr + label := "demo contract to query" + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, contract.CodeID, contract.CreatorAddr, adminAddr, initMsgBz, label, initialAmount) + require.NoError(t, err) + return HackatomExampleInstance{ + ExampleContract: contract, + Contract: contractAddr, + Verifier: verifier, + VerifierAddr: verifierAddr, + Beneficiary: beneficiary, + BeneficiaryAddr: beneficiaryAddr, + Label: label, + Deposit: initialAmount, + } +} + +type ExampleInstance struct { + ExampleContract + Contract sdk.AccAddress + Label string + Deposit sdk.Coins +} + +// InstantiateReflectExampleContract load and instantiate the "./testdata/reflect.wasm" contract +func InstantiateReflectExampleContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) ExampleInstance { + example := StoreReflectContract(t, ctx, keepers) + initialAmount := sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + label := "demo contract to query" + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, example.CodeID, example.CreatorAddr, example.CreatorAddr, []byte("{}"), label, initialAmount) + + require.NoError(t, err) + return ExampleInstance{ + ExampleContract: example, + Contract: contractAddr, + Label: label, + Deposit: initialAmount, + } +} + +type HackatomExampleInitMsg struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` +} + +func (m HackatomExampleInitMsg) GetBytes(t testing.TB) []byte { + initMsgBz, err := json.Marshal(m) + require.NoError(t, err) + return initMsgBz +} + +type IBCReflectExampleInstance struct { + Contract sdk.AccAddress + Admin sdk.AccAddress + CodeID uint64 + ReflectCodeID uint64 +} + +// InstantiateIBCReflectContract load and instantiate the "./testdata/ibc_reflect.wasm" contract +func InstantiateIBCReflectContract(t testing.TB, ctx sdk.Context, keepers TestKeepers) IBCReflectExampleInstance { + reflectID := StoreReflectContract(t, ctx, keepers).CodeID + ibcReflectID := StoreIBCReflectContract(t, ctx, keepers).CodeID + + initMsgBz := IBCReflectInitMsg{ + ReflectCodeID: reflectID, + }.GetBytes(t) + adminAddr := RandomAccountAddress(t) + + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, ibcReflectID, adminAddr, adminAddr, initMsgBz, "ibc-reflect-factory", nil) + require.NoError(t, err) + return IBCReflectExampleInstance{ + Admin: adminAddr, + Contract: contractAddr, + CodeID: ibcReflectID, + ReflectCodeID: reflectID, + } +} + +type IBCReflectInitMsg struct { + ReflectCodeID uint64 `json:"reflect_code_id"` +} + +func (m IBCReflectInitMsg) GetBytes(t testing.TB) []byte { + initMsgBz, err := json.Marshal(m) + require.NoError(t, err) + return initMsgBz +} + +type BurnerExampleInitMsg struct { + Payout sdk.AccAddress `json:"payout"` +} + +func (m BurnerExampleInitMsg) GetBytes(t testing.TB) []byte { + initMsgBz, err := json.Marshal(m) + require.NoError(t, err) + return initMsgBz +} + +func fundAccounts(t testing.TB, ctx sdk.Context, am authkeeper.AccountKeeper, bank bankkeeper.Keeper, addr sdk.AccAddress, coins sdk.Coins) { + acc := am.NewAccountWithAddress(ctx, addr) + am.SetAccount(ctx, acc) + NewTestFaucet(t, ctx, bank, minttypes.ModuleName, coins...).Fund(ctx, addr, coins...) +} + +var keyCounter uint64 + +// we need to make this deterministic (same every test run), as encoded address size and thus gas cost, +// depends on the actual bytes (due to ugly CanonicalAddress encoding) +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { + keyCounter++ + seed := make([]byte, 8) + binary.BigEndian.PutUint64(seed, keyCounter) + + key := ed25519.GenPrivKeyFromSecret(seed) + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + return key, pub, addr +} diff --git a/x/wasm/keeper/test_fuzz.go b/x/wasm/keeper/test_fuzz.go new file mode 100644 index 00000000..cca94eb7 --- /dev/null +++ b/x/wasm/keeper/test_fuzz.go @@ -0,0 +1,76 @@ +package keeper + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + fuzz "github.com/google/gofuzz" + tmBytes "github.com/tendermint/tendermint/libs/bytes" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var ModelFuzzers = []interface{}{FuzzAddr, FuzzAddrString, FuzzAbsoluteTxPosition, FuzzContractInfo, FuzzStateModel, FuzzAccessType, FuzzAccessConfig, FuzzContractCodeHistory} + +func FuzzAddr(m *sdk.AccAddress, c fuzz.Continue) { + *m = make([]byte, 20) + c.Read(*m) +} + +func FuzzAddrString(m *string, c fuzz.Continue) { + var x sdk.AccAddress + FuzzAddr(&x, c) + *m = x.String() +} + +func FuzzAbsoluteTxPosition(m *types.AbsoluteTxPosition, c fuzz.Continue) { + m.BlockHeight = c.RandUint64() + m.TxIndex = c.RandUint64() +} + +func FuzzContractInfo(m *types.ContractInfo, c fuzz.Continue) { + m.CodeID = c.RandUint64() + FuzzAddrString(&m.Creator, c) + FuzzAddrString(&m.Admin, c) + m.Label = c.RandString() + c.Fuzz(&m.Created) +} + +func FuzzContractCodeHistory(m *types.ContractCodeHistoryEntry, c fuzz.Continue) { + const maxMsgSize = 128 + m.CodeID = c.RandUint64() + msg := make([]byte, c.RandUint64()%maxMsgSize) + c.Read(msg) + var err error + if m.Msg, err = json.Marshal(msg); err != nil { + panic(err) + } + c.Fuzz(&m.Updated) + m.Operation = types.AllCodeHistoryTypes[c.Int()%len(types.AllCodeHistoryTypes)] +} + +func FuzzStateModel(m *types.Model, c fuzz.Continue) { + m.Key = tmBytes.HexBytes(c.RandString()) + if len(m.Key) == 0 { + m.Key = tmBytes.HexBytes("non empty key") + } + c.Fuzz(&m.Value) +} + +func FuzzAccessType(m *types.AccessType, c fuzz.Continue) { + pos := c.Int() % len(types.AllAccessTypes) + for _, v := range types.AllAccessTypes { + if pos == 0 { + *m = v + return + } + pos-- + } +} + +func FuzzAccessConfig(m *types.AccessConfig, c fuzz.Continue) { + FuzzAccessType(&m.Permission, c) + var add sdk.AccAddress + FuzzAddr(&add, c) + *m = m.Permission.With(add) +} diff --git a/x/wasm/keeper/testdata/broken_crc.gzip b/x/wasm/keeper/testdata/broken_crc.gzip new file mode 100644 index 00000000..378713e2 Binary files /dev/null and b/x/wasm/keeper/testdata/broken_crc.gzip differ diff --git a/x/wasm/keeper/testdata/burner.wasm b/x/wasm/keeper/testdata/burner.wasm new file mode 100644 index 00000000..66390756 Binary files /dev/null and b/x/wasm/keeper/testdata/burner.wasm differ diff --git a/x/wasm/keeper/testdata/download_releases.sh b/x/wasm/keeper/testdata/download_releases.sh new file mode 100755 index 00000000..27576139 --- /dev/null +++ b/x/wasm/keeper/testdata/download_releases.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -o errexit -o nounset -o pipefail +command -v shellcheck > /dev/null && shellcheck "$0" + +if [ $# -ne 1 ]; then + echo "Usage: ./download_releases.sh RELEASE_TAG" + exit 1 +fi + +tag="$1" + +for contract in burner hackatom ibc_reflect ibc_reflect_send reflect staking; do + url="https://github.com/CosmWasm/cosmwasm/releases/download/$tag/${contract}.wasm" + echo "Downloading $url ..." + wget -O "${contract}.wasm" "$url" +done + +# create the zip variant +gzip -k hackatom.wasm +mv hackatom.wasm.gz hackatom.wasm.gzip + +rm -f version.txt +echo "$tag" >version.txt \ No newline at end of file diff --git a/x/wasm/keeper/testdata/genesis.json b/x/wasm/keeper/testdata/genesis.json new file mode 100644 index 00000000..08969c7d --- /dev/null +++ b/x/wasm/keeper/testdata/genesis.json @@ -0,0 +1,219 @@ +{ + "genesis_time": "2020-07-13T07:49:08.2945876Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "upgrade": {}, + "evidence": { + "params": { + "max_evidence_age": "120000000000" + }, + "evidence": [] + }, + "supply": { + "supply": [] + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "ustake", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + }, + "gov": { + "starting_proposal_id": "1", + "deposits": null, + "votes": null, + "proposals": null, + "deposit_params": { + "min_deposit": [ + { + "denom": "ustake", + "amount": "1" + } + ], + "max_deposit_period": "172800000000000" + }, + "voting_params": { + "voting_period": "60000000000", + "voting_period_desc": "1minute" + }, + "tally_params": { + "quorum": "0.000000000000000001", + "threshold": "0.000000000000000001", + "veto": "0.334000000000000000" + } + }, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600000000000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": {}, + "missed_blocks": {} + }, + "wasm": { + "params": { + "upload_access": { + "type": 3, + "address": "" + }, + "instantiate_default_permission": 3 + }, + "codes": null, + "contracts": null, + "sequences": null + }, + "bank": { + "send_enabled": true + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "crisis": { + "constant_fee": { + "denom": "ustake", + "amount": "1000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "testing", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "validator_address": "cosmosvaloper1ve557a5g9yw2g2z57js3pdmcvd5my6g88d76lj", + "pubkey": "cosmosvalconspub1zcjduepqddfln4tujr2p8actpgqz4h2xnls9y7tu9c9tu5lqkdglmdjalzuqah4neg", + "value": { + "denom": "ustake", + "amount": "250000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A//cqZxkpH1re0VrHBtH308nb5t8K+Y/hF0GeRdRBmaJ" + }, + "signature": "5QEEIuUVQTEBMuAtOOHnnKo6rPsIbmfzUxUqRnDFERVqwVr1Kg+ex4f/UGIK0yrOAvOG8zDADwFP4yF8lw+o5g==" + } + ], + "memo": "836fc54e9cad58f4ed6420223ec6290f75342afa@172.17.0.2:26656" + } + } + ] + }, + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "coins": [ + { + "denom": "ucosm", + "amount": "1000000000" + }, + { + "denom": "ustake", + "amount": "1000000000" + } + ], + "public_key": "", + "account_number": 0, + "sequence": 0 + } + } + ] + }, + "params": null, + "staking": { + "params": { + "unbonding_time": "1814400000000000", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 0, + "bond_denom": "ustake" + }, + "last_total_power": "0", + "last_validator_powers": null, + "validators": null, + "delegations": null, + "unbonding_delegations": null, + "redelegations": null, + "exported": false + } + } +} \ No newline at end of file diff --git a/x/wasm/keeper/testdata/hackatom.wasm b/x/wasm/keeper/testdata/hackatom.wasm new file mode 100644 index 00000000..baa03a85 Binary files /dev/null and b/x/wasm/keeper/testdata/hackatom.wasm differ diff --git a/x/wasm/keeper/testdata/hackatom.wasm.gzip b/x/wasm/keeper/testdata/hackatom.wasm.gzip new file mode 100644 index 00000000..3c95e9b1 Binary files /dev/null and b/x/wasm/keeper/testdata/hackatom.wasm.gzip differ diff --git a/x/wasm/keeper/testdata/ibc_reflect.wasm b/x/wasm/keeper/testdata/ibc_reflect.wasm new file mode 100644 index 00000000..ec737104 Binary files /dev/null and b/x/wasm/keeper/testdata/ibc_reflect.wasm differ diff --git a/x/wasm/keeper/testdata/ibc_reflect_send.wasm b/x/wasm/keeper/testdata/ibc_reflect_send.wasm new file mode 100644 index 00000000..0f7d7e45 Binary files /dev/null and b/x/wasm/keeper/testdata/ibc_reflect_send.wasm differ diff --git a/x/wasm/keeper/testdata/reflect.go b/x/wasm/keeper/testdata/reflect.go new file mode 100644 index 00000000..64fed615 --- /dev/null +++ b/x/wasm/keeper/testdata/reflect.go @@ -0,0 +1,69 @@ +package testdata + +import ( + _ "embed" + + typwasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/types" +) + +//go:embed reflect.wasm +var reflectContract []byte + +//go:embed reflect_1_1.wasm +var migrateReflectContract []byte + +func ReflectContractWasm() []byte { + return reflectContract +} + +func MigrateReflectContractWasm() []byte { + return migrateReflectContract +} + +// ReflectHandleMsg is used to encode handle messages +type ReflectHandleMsg struct { + Reflect *ReflectPayload `json:"reflect_msg,omitempty"` + ReflectSubMsg *ReflectSubPayload `json:"reflect_sub_msg,omitempty"` + ChangeOwner *OwnerPayload `json:"change_owner,omitempty"` +} + +type OwnerPayload struct { + Owner types.Address `json:"owner"` +} + +type ReflectPayload struct { + Msgs []typwasmvmtypes.CosmosMsg `json:"msgs"` +} + +type ReflectSubPayload struct { + Msgs []typwasmvmtypes.SubMsg `json:"msgs"` +} + +// ReflectQueryMsg is used to encode query messages +type ReflectQueryMsg struct { + Owner *struct{} `json:"owner,omitempty"` + Capitalized *Text `json:"capitalized,omitempty"` + Chain *ChainQuery `json:"chain,omitempty"` + SubMsgResult *SubCall `json:"sub_msg_result,omitempty"` +} + +type ChainQuery struct { + Request *typwasmvmtypes.QueryRequest `json:"request,omitempty"` +} + +type Text struct { + Text string `json:"text"` +} + +type SubCall struct { + ID uint64 `json:"id"` +} + +type OwnerResponse struct { + Owner string `json:"owner,omitempty"` +} + +type ChainResponse struct { + Data []byte `json:"data,omitempty"` +} diff --git a/x/wasm/keeper/testdata/reflect.wasm b/x/wasm/keeper/testdata/reflect.wasm new file mode 100644 index 00000000..31735645 Binary files /dev/null and b/x/wasm/keeper/testdata/reflect.wasm differ diff --git a/x/wasm/keeper/testdata/reflect.wasm.v1_0 b/x/wasm/keeper/testdata/reflect.wasm.v1_0 new file mode 100644 index 00000000..312f4576 Binary files /dev/null and b/x/wasm/keeper/testdata/reflect.wasm.v1_0 differ diff --git a/x/wasm/keeper/testdata/reflect_1_1.wasm b/x/wasm/keeper/testdata/reflect_1_1.wasm new file mode 100644 index 00000000..7383a6d6 Binary files /dev/null and b/x/wasm/keeper/testdata/reflect_1_1.wasm differ diff --git a/x/wasm/keeper/testdata/staking.wasm b/x/wasm/keeper/testdata/staking.wasm new file mode 100644 index 00000000..015ae00e Binary files /dev/null and b/x/wasm/keeper/testdata/staking.wasm differ diff --git a/x/wasm/keeper/testdata/version.txt b/x/wasm/keeper/testdata/version.txt new file mode 100644 index 00000000..79127d85 --- /dev/null +++ b/x/wasm/keeper/testdata/version.txt @@ -0,0 +1 @@ +v1.2.0 diff --git a/x/wasm/keeper/wasmtesting/extension_mocks.go b/x/wasm/keeper/wasmtesting/extension_mocks.go new file mode 100644 index 00000000..562d9e74 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/extension_mocks.go @@ -0,0 +1,28 @@ +package wasmtesting + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +type MockCoinTransferrer struct { + TransferCoinsFn func(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error +} + +func (m *MockCoinTransferrer) TransferCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error { + if m.TransferCoinsFn == nil { + panic("not expected to be called") + } + return m.TransferCoinsFn(ctx, fromAddr, toAddr, amt) +} + +type AccountPrunerMock struct { + CleanupExistingAccountFn func(ctx sdk.Context, existingAccount authtypes.AccountI) (handled bool, err error) +} + +func (m AccountPrunerMock) CleanupExistingAccount(ctx sdk.Context, existingAccount authtypes.AccountI) (handled bool, err error) { + if m.CleanupExistingAccountFn == nil { + panic("not expected to be called") + } + return m.CleanupExistingAccountFn(ctx, existingAccount) +} diff --git a/x/wasm/keeper/wasmtesting/gas_register.go b/x/wasm/keeper/wasmtesting/gas_register.go new file mode 100644 index 00000000..d1975f76 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/gas_register.go @@ -0,0 +1,74 @@ +package wasmtesting + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MockGasRegister mock that implements keeper.GasRegister +type MockGasRegister struct { + CompileCostFn func(byteLength int) sdk.Gas + NewContractInstanceCostFn func(pinned bool, msgLen int) sdk.Gas + InstantiateContractCostFn func(pinned bool, msgLen int) sdk.Gas + ReplyCostFn func(pinned bool, reply wasmvmtypes.Reply) sdk.Gas + EventCostsFn func(evts []wasmvmtypes.EventAttribute) sdk.Gas + ToWasmVMGasFn func(source sdk.Gas) uint64 + FromWasmVMGasFn func(source uint64) sdk.Gas + UncompressCostsFn func(byteLength int) sdk.Gas +} + +func (m MockGasRegister) NewContractInstanceCosts(pinned bool, msgLen int) sdk.Gas { + if m.NewContractInstanceCostFn == nil { + panic("not expected to be called") + } + return m.NewContractInstanceCostFn(pinned, msgLen) +} + +func (m MockGasRegister) CompileCosts(byteLength int) sdk.Gas { + if m.CompileCostFn == nil { + panic("not expected to be called") + } + return m.CompileCostFn(byteLength) +} + +func (m MockGasRegister) UncompressCosts(byteLength int) sdk.Gas { + if m.UncompressCostsFn == nil { + panic("not expected to be called") + } + return m.UncompressCostsFn(byteLength) +} + +func (m MockGasRegister) InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas { + if m.InstantiateContractCostFn == nil { + panic("not expected to be called") + } + return m.InstantiateContractCostFn(pinned, msgLen) +} + +func (m MockGasRegister) ReplyCosts(pinned bool, reply wasmvmtypes.Reply) sdk.Gas { + if m.ReplyCostFn == nil { + panic("not expected to be called") + } + return m.ReplyCostFn(pinned, reply) +} + +func (m MockGasRegister) EventCosts(evts []wasmvmtypes.EventAttribute, events wasmvmtypes.Events) sdk.Gas { + if m.EventCostsFn == nil { + panic("not expected to be called") + } + return m.EventCostsFn(evts) +} + +func (m MockGasRegister) ToWasmVMGas(source sdk.Gas) uint64 { + if m.ToWasmVMGasFn == nil { + panic("not expected to be called") + } + return m.ToWasmVMGasFn(source) +} + +func (m MockGasRegister) FromWasmVMGas(source uint64) sdk.Gas { + if m.FromWasmVMGasFn == nil { + panic("not expected to be called") + } + return m.FromWasmVMGasFn(source) +} diff --git a/x/wasm/keeper/wasmtesting/message_router.go b/x/wasm/keeper/wasmtesting/message_router.go new file mode 100644 index 00000000..712e012d --- /dev/null +++ b/x/wasm/keeper/wasmtesting/message_router.go @@ -0,0 +1,27 @@ +package wasmtesting + +import ( + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MockMessageRouter mock for testing +type MockMessageRouter struct { + HandlerFn func(msg sdk.Msg) baseapp.MsgServiceHandler +} + +// Handler is the entry point +func (m MockMessageRouter) Handler(msg sdk.Msg) baseapp.MsgServiceHandler { + if m.HandlerFn == nil { + panic("not expected to be called") + } + return m.HandlerFn(msg) +} + +// MessageRouterFunc convenient type to match the keeper.MessageRouter interface +type MessageRouterFunc func(msg sdk.Msg) baseapp.MsgServiceHandler + +// Handler is the entry point +func (m MessageRouterFunc) Handler(msg sdk.Msg) baseapp.MsgServiceHandler { + return m(msg) +} diff --git a/x/wasm/keeper/wasmtesting/messenger.go b/x/wasm/keeper/wasmtesting/messenger.go new file mode 100644 index 00000000..dbafda38 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/messenger.go @@ -0,0 +1,38 @@ +package wasmtesting + +import ( + "errors" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type MockMessageHandler struct { + DispatchMsgFn func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) +} + +func (m *MockMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + if m.DispatchMsgFn == nil { + panic("not expected to be called") + } + return m.DispatchMsgFn(ctx, contractAddr, contractIBCPortID, msg) +} + +func NewCapturingMessageHandler() (*MockMessageHandler, *[]wasmvmtypes.CosmosMsg) { + var messages []wasmvmtypes.CosmosMsg + return &MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + messages = append(messages, msg) + // return one data item so that this doesn't cause an error in submessage processing (it takes the first element from data) + return nil, [][]byte{{1}}, nil + }, + }, &messages +} + +func NewErroringMessageHandler() *MockMessageHandler { + return &MockMessageHandler{ + DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + return nil, nil, errors.New("test, ignore") + }, + } +} diff --git a/x/wasm/keeper/wasmtesting/mock_engine.go b/x/wasm/keeper/wasmtesting/mock_engine.go new file mode 100644 index 00000000..2c0b4a98 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/mock_engine.go @@ -0,0 +1,401 @@ +package wasmtesting + +import ( + "bytes" + "crypto/sha256" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/tendermint/tendermint/libs/rand" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var _ types.WasmerEngine = &MockWasmer{} + +// MockWasmer implements types.WasmerEngine for testing purpose. One or multiple messages can be stubbed. +// Without a stub function a panic is thrown. +type MockWasmer struct { + CreateFn func(codeID wasmvm.WasmCode) (wasmvm.Checksum, error) + AnalyzeCodeFn func(codeID wasmvm.Checksum) (*wasmvmtypes.AnalysisReport, error) + InstantiateFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + ExecuteFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + QueryFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) + MigrateFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, migrateMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + SudoFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + ReplyFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + GetCodeFn func(codeID wasmvm.Checksum) (wasmvm.WasmCode, error) + CleanupFn func() + IBCChannelOpenFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + IBCChannelConnectFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + IBCChannelCloseFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + IBCPacketReceiveFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) + IBCPacketAckFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + IBCPacketTimeoutFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + PinFn func(checksum wasmvm.Checksum) error + UnpinFn func(checksum wasmvm.Checksum) error + GetMetricsFn func() (*wasmvmtypes.Metrics, error) +} + +func (m *MockWasmer) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + if m.IBCChannelOpenFn == nil { + panic("not supposed to be called!") + } + return m.IBCChannelOpenFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) IBCChannelConnect(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCChannelConnectFn == nil { + panic("not supposed to be called!") + } + return m.IBCChannelConnectFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) IBCChannelClose(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCChannelCloseFn == nil { + panic("not supposed to be called!") + } + return m.IBCChannelCloseFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + if m.IBCPacketReceiveFn == nil { + panic("not supposed to be called!") + } + return m.IBCPacketReceiveFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) IBCPacketAck(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCPacketAckFn == nil { + panic("not supposed to be called!") + } + return m.IBCPacketAckFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) IBCPacketTimeout(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCPacketTimeoutFn == nil { + panic("not supposed to be called!") + } + return m.IBCPacketTimeoutFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) Create(codeID wasmvm.WasmCode) (wasmvm.Checksum, error) { + if m.CreateFn == nil { + panic("not supposed to be called!") + } + return m.CreateFn(codeID) +} + +func (m *MockWasmer) AnalyzeCode(codeID wasmvm.Checksum) (*wasmvmtypes.AnalysisReport, error) { + if m.AnalyzeCodeFn == nil { + panic("not supposed to be called!") + } + return m.AnalyzeCodeFn(codeID) +} + +func (m *MockWasmer) Instantiate(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + if m.InstantiateFn == nil { + panic("not supposed to be called!") + } + return m.InstantiateFn(codeID, env, info, initMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) Execute(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + if m.ExecuteFn == nil { + panic("not supposed to be called!") + } + return m.ExecuteFn(codeID, env, info, executeMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) Query(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) ([]byte, uint64, error) { + if m.QueryFn == nil { + panic("not supposed to be called!") + } + return m.QueryFn(codeID, env, queryMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) Migrate(codeID wasmvm.Checksum, env wasmvmtypes.Env, migrateMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + if m.MigrateFn == nil { + panic("not supposed to be called!") + } + return m.MigrateFn(codeID, env, migrateMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) Sudo(codeID wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + if m.SudoFn == nil { + panic("not supposed to be called!") + } + return m.SudoFn(codeID, env, sudoMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) Reply(codeID wasmvm.Checksum, env wasmvmtypes.Env, reply wasmvmtypes.Reply, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + if m.ReplyFn == nil { + panic("not supposed to be called!") + } + return m.ReplyFn(codeID, env, reply, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m *MockWasmer) GetCode(codeID wasmvm.Checksum) (wasmvm.WasmCode, error) { + if m.GetCodeFn == nil { + panic("not supposed to be called!") + } + return m.GetCodeFn(codeID) +} + +func (m *MockWasmer) Cleanup() { + if m.CleanupFn == nil { + panic("not supposed to be called!") + } + m.CleanupFn() +} + +func (m *MockWasmer) Pin(checksum wasmvm.Checksum) error { + if m.PinFn == nil { + panic("not supposed to be called!") + } + return m.PinFn(checksum) +} + +func (m *MockWasmer) Unpin(checksum wasmvm.Checksum) error { + if m.UnpinFn == nil { + panic("not supposed to be called!") + } + return m.UnpinFn(checksum) +} + +func (m *MockWasmer) GetMetrics() (*wasmvmtypes.Metrics, error) { + if m.GetMetricsFn == nil { + panic("not expected to be called") + } + return m.GetMetricsFn() +} + +var AlwaysPanicMockWasmer = &MockWasmer{} + +// SelfCallingInstMockWasmer prepares a Wasmer mock that calls itself on instantiation. +func SelfCallingInstMockWasmer(executeCalled *bool) *MockWasmer { + return &MockWasmer{ + CreateFn: func(code wasmvm.WasmCode) (wasmvm.Checksum, error) { + anyCodeID := bytes.Repeat([]byte{0x1}, 32) + return anyCodeID, nil + }, + InstantiateFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{ + Messages: []wasmvmtypes.SubMsg{ + {Msg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{Execute: &wasmvmtypes.ExecuteMsg{ContractAddr: env.Contract.Address, Msg: []byte(`{}`)}}, + }}, + }, + }, 1, nil + }, + AnalyzeCodeFn: WithoutIBCAnalyzeFn, + ExecuteFn: func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + *executeCalled = true + return &wasmvmtypes.Response{}, 1, nil + }, + } +} + +// IBCContractCallbacks defines the methods from wasmvm to interact with the wasm contract. +// A mock contract would implement the interface to fully simulate a wasm contract's behaviour. +type IBCContractCallbacks interface { + IBCChannelOpen( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + channel wasmvmtypes.IBCChannelOpenMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + + IBCChannelConnect( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + channel wasmvmtypes.IBCChannelConnectMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + IBCChannelClose( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + channel wasmvmtypes.IBCChannelCloseMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + IBCPacketReceive( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + packet wasmvmtypes.IBCPacketReceiveMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCReceiveResult, uint64, error) + + IBCPacketAck( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + ack wasmvmtypes.IBCPacketAckMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + IBCPacketTimeout( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + packet wasmvmtypes.IBCPacketTimeoutMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) +} + +type contractExecutable interface { + Execute( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + info wasmvmtypes.MessageInfo, + executeMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) +} + +// MakeInstantiable adds some noop functions to not fail when contract is used for instantiation +func MakeInstantiable(m *MockWasmer) { + m.CreateFn = HashOnlyCreateFn + m.InstantiateFn = NoOpInstantiateFn + m.AnalyzeCodeFn = WithoutIBCAnalyzeFn +} + +// MakeIBCInstantiable adds some noop functions to not fail when contract is used for instantiation +func MakeIBCInstantiable(m *MockWasmer) { + MakeInstantiable(m) + m.AnalyzeCodeFn = HasIBCAnalyzeFn +} + +// NewIBCContractMockWasmer prepares a mocked wasm_engine for testing with an IBC contract test type. +// It is safe to use the mock with store code and instantiate functions in keeper as is also prepared +// with stubs. Execute is optional. When implemented by the Go test contract then it can be used with +// the mock. +func NewIBCContractMockWasmer(c IBCContractCallbacks) *MockWasmer { + m := &MockWasmer{ + IBCChannelOpenFn: c.IBCChannelOpen, + IBCChannelConnectFn: c.IBCChannelConnect, + IBCChannelCloseFn: c.IBCChannelClose, + IBCPacketReceiveFn: c.IBCPacketReceive, + IBCPacketAckFn: c.IBCPacketAck, + IBCPacketTimeoutFn: c.IBCPacketTimeout, + } + MakeIBCInstantiable(m) + if e, ok := c.(contractExecutable); ok { // optional function + m.ExecuteFn = e.Execute + } + return m +} + +func HashOnlyCreateFn(code wasmvm.WasmCode) (wasmvm.Checksum, error) { + if code == nil { + return nil, sdkerrors.Wrap(types.ErrInvalid, "wasm code must not be nil") + } + hash := sha256.Sum256(code) + return hash[:], nil +} + +func NoOpInstantiateFn(wasmvm.Checksum, wasmvmtypes.Env, wasmvmtypes.MessageInfo, []byte, wasmvm.KVStore, wasmvm.GoAPI, wasmvm.Querier, wasmvm.GasMeter, uint64, wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return &wasmvmtypes.Response{}, 0, nil +} + +func NoOpCreateFn(_ wasmvm.WasmCode) (wasmvm.Checksum, error) { + return rand.Bytes(32), nil +} + +func HasIBCAnalyzeFn(wasmvm.Checksum) (*wasmvmtypes.AnalysisReport, error) { + return &wasmvmtypes.AnalysisReport{ + HasIBCEntryPoints: true, + }, nil +} + +func WithoutIBCAnalyzeFn(wasmvm.Checksum) (*wasmvmtypes.AnalysisReport, error) { + return &wasmvmtypes.AnalysisReport{}, nil +} + +var _ IBCContractCallbacks = &MockIBCContractCallbacks{} + +type MockIBCContractCallbacks struct { + IBCChannelOpenFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + IBCChannelConnectFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + IBCChannelCloseFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + IBCPacketReceiveFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) + IBCPacketAckFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) + IBCPacketTimeoutFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) +} + +func (m MockIBCContractCallbacks) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, channel wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + if m.IBCChannelOpenFn == nil { + panic("not expected to be called") + } + return m.IBCChannelOpenFn(codeID, env, channel, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m MockIBCContractCallbacks) IBCChannelConnect(codeID wasmvm.Checksum, env wasmvmtypes.Env, channel wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCChannelConnectFn == nil { + panic("not expected to be called") + } + return m.IBCChannelConnectFn(codeID, env, channel, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m MockIBCContractCallbacks) IBCChannelClose(codeID wasmvm.Checksum, env wasmvmtypes.Env, channel wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCChannelCloseFn == nil { + panic("not expected to be called") + } + return m.IBCChannelCloseFn(codeID, env, channel, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m MockIBCContractCallbacks) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, packet wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + if m.IBCPacketReceiveFn == nil { + panic("not expected to be called") + } + return m.IBCPacketReceiveFn(codeID, env, packet, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m MockIBCContractCallbacks) IBCPacketAck(codeID wasmvm.Checksum, env wasmvmtypes.Env, ack wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCPacketAckFn == nil { + panic("not expected to be called") + } + return m.IBCPacketAckFn(codeID, env, ack, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +func (m MockIBCContractCallbacks) IBCPacketTimeout(codeID wasmvm.Checksum, env wasmvmtypes.Env, packet wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + if m.IBCPacketTimeoutFn == nil { + panic("not expected to be called") + } + return m.IBCPacketTimeoutFn(codeID, env, packet, store, goapi, querier, gasMeter, gasLimit, deserCost) +} diff --git a/x/wasm/keeper/wasmtesting/mock_keepers.go b/x/wasm/keeper/wasmtesting/mock_keepers.go new file mode 100644 index 00000000..7020c706 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/mock_keepers.go @@ -0,0 +1,120 @@ +package wasmtesting + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +type MockChannelKeeper struct { + GetChannelFn func(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) + GetNextSequenceSendFn func(ctx sdk.Context, portID, channelID string) (uint64, bool) + SendPacketFn func(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error + ChanCloseInitFn func(ctx sdk.Context, portID, channelID string, chanCap *capabilitytypes.Capability) error + GetAllChannelsFn func(ctx sdk.Context) []channeltypes.IdentifiedChannel + IterateChannelsFn func(ctx sdk.Context, cb func(channeltypes.IdentifiedChannel) bool) + SetChannelFn func(ctx sdk.Context, portID, channelID string, channel channeltypes.Channel) +} + +func (m *MockChannelKeeper) GetChannel(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) { + if m.GetChannelFn == nil { + panic("not supposed to be called!") + } + return m.GetChannelFn(ctx, srcPort, srcChan) +} + +func (m *MockChannelKeeper) GetAllChannels(ctx sdk.Context) []channeltypes.IdentifiedChannel { + if m.GetAllChannelsFn == nil { + panic("not supposed to be called!") + } + return m.GetAllChannelsFn(ctx) +} + +func (m *MockChannelKeeper) GetNextSequenceSend(ctx sdk.Context, portID, channelID string) (uint64, bool) { + if m.GetNextSequenceSendFn == nil { + panic("not supposed to be called!") + } + return m.GetNextSequenceSendFn(ctx, portID, channelID) +} + +func (m *MockChannelKeeper) SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error { + if m.SendPacketFn == nil { + panic("not supposed to be called!") + } + return m.SendPacketFn(ctx, channelCap, packet) +} + +func (m *MockChannelKeeper) ChanCloseInit(ctx sdk.Context, portID, channelID string, chanCap *capabilitytypes.Capability) error { + if m.ChanCloseInitFn == nil { + panic("not supposed to be called!") + } + return m.ChanCloseInitFn(ctx, portID, channelID, chanCap) +} + +func (m *MockChannelKeeper) IterateChannels(ctx sdk.Context, cb func(channeltypes.IdentifiedChannel) bool) { + if m.IterateChannelsFn == nil { + panic("not expected to be called") + } + m.IterateChannelsFn(ctx, cb) +} + +func (m *MockChannelKeeper) SetChannel(ctx sdk.Context, portID, channelID string, channel channeltypes.Channel) { + if m.GetChannelFn == nil { + panic("not supposed to be called!") + } + m.SetChannelFn(ctx, portID, channelID, channel) +} + +func MockChannelKeeperIterator(s []channeltypes.IdentifiedChannel) func(ctx sdk.Context, cb func(channeltypes.IdentifiedChannel) bool) { + return func(ctx sdk.Context, cb func(channeltypes.IdentifiedChannel) bool) { + for _, channel := range s { + stop := cb(channel) + if stop { + break + } + } + } +} + +type MockCapabilityKeeper struct { + GetCapabilityFn func(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) + ClaimCapabilityFn func(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error + AuthenticateCapabilityFn func(ctx sdk.Context, capability *capabilitytypes.Capability, name string) bool +} + +func (m MockCapabilityKeeper) GetCapability(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) { + if m.GetCapabilityFn == nil { + panic("not supposed to be called!") + } + return m.GetCapabilityFn(ctx, name) +} + +func (m MockCapabilityKeeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error { + if m.ClaimCapabilityFn == nil { + panic("not supposed to be called!") + } + return m.ClaimCapabilityFn(ctx, cap, name) +} + +func (m MockCapabilityKeeper) AuthenticateCapability(ctx sdk.Context, capability *capabilitytypes.Capability, name string) bool { + if m.AuthenticateCapabilityFn == nil { + panic("not supposed to be called!") + } + return m.AuthenticateCapabilityFn(ctx, capability, name) +} + +var _ types.ICS20TransferPortSource = &MockIBCTransferKeeper{} + +type MockIBCTransferKeeper struct { + GetPortFn func(ctx sdk.Context) string +} + +func (m MockIBCTransferKeeper) GetPort(ctx sdk.Context) string { + if m.GetPortFn == nil { + panic("not expected to be called") + } + return m.GetPortFn(ctx) +} diff --git a/x/wasm/keeper/wasmtesting/msg_dispatcher.go b/x/wasm/keeper/wasmtesting/msg_dispatcher.go new file mode 100644 index 00000000..a6600941 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/msg_dispatcher.go @@ -0,0 +1,17 @@ +package wasmtesting + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type MockMsgDispatcher struct { + DispatchSubmessagesFn func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) +} + +func (m MockMsgDispatcher) DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) { + if m.DispatchSubmessagesFn == nil { + panic("not expected to be called") + } + return m.DispatchSubmessagesFn(ctx, contractAddr, ibcPort, msgs) +} diff --git a/x/wasm/keeper/wasmtesting/query_handler.go b/x/wasm/keeper/wasmtesting/query_handler.go new file mode 100644 index 00000000..52cf97d3 --- /dev/null +++ b/x/wasm/keeper/wasmtesting/query_handler.go @@ -0,0 +1,17 @@ +package wasmtesting + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type MockQueryHandler struct { + HandleQueryFn func(ctx sdk.Context, request wasmvmtypes.QueryRequest, caller sdk.AccAddress) ([]byte, error) +} + +func (m *MockQueryHandler) HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { + if m.HandleQueryFn == nil { + panic("not expected to be called") + } + return m.HandleQueryFn(ctx, request, caller) +} diff --git a/x/wasm/keeper/wasmtesting/store.go b/x/wasm/keeper/wasmtesting/store.go new file mode 100644 index 00000000..6548c8ba --- /dev/null +++ b/x/wasm/keeper/wasmtesting/store.go @@ -0,0 +1,26 @@ +package wasmtesting + +import ( + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MockCommitMultiStore mock with a CacheMultiStore to capture commits +type MockCommitMultiStore struct { + sdk.CommitMultiStore + Committed []bool +} + +func (m *MockCommitMultiStore) CacheMultiStore() storetypes.CacheMultiStore { + m.Committed = append(m.Committed, false) + return &mockCMS{m, &m.Committed[len(m.Committed)-1]} +} + +type mockCMS struct { + sdk.CommitMultiStore + committed *bool +} + +func (m *mockCMS) Write() { + *m.committed = true +} diff --git a/x/wasm/module.go b/x/wasm/module.go new file mode 100644 index 00000000..e73288ca --- /dev/null +++ b/x/wasm/module.go @@ -0,0 +1,311 @@ +package wasm + +import ( + "context" + "encoding/json" + "fmt" + "math/rand" + "runtime/debug" + "strings" + + wasmvm "github.com/CosmWasm/wasmvm" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cast" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cerc-io/laconicd/x/wasm/client/cli" + "github.com/cerc-io/laconicd/x/wasm/client/rest" //nolint:staticcheck + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/simulation" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// Module init related flags +const ( + flagWasmMemoryCacheSize = "wasm.memory_cache_size" + flagWasmQueryGasLimit = "wasm.query_gas_limit" + flagWasmSimulationGasLimit = "wasm.simulation_gas_limit" +) + +// AppModuleBasic defines the basic application module used by the wasm module. +type AppModuleBasic struct{} + +func (b AppModuleBasic) RegisterLegacyAminoCodec(amino *codec.LegacyAmino) { //nolint:staticcheck + RegisterCodec(amino) +} + +func (b AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, serveMux *runtime.ServeMux) { + err := types.RegisterQueryHandlerClient(context.Background(), serveMux, types.NewQueryClient(clientCtx)) + if err != nil { + panic(err) + } +} + +// Name returns the wasm module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// DefaultGenesis returns default genesis state as raw bytes for the wasm +// module. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(&GenesisState{ + Params: DefaultParams(), + }) +} + +// ValidateGenesis performs genesis state validation for the wasm module. +func (b AppModuleBasic) ValidateGenesis(marshaler codec.JSONCodec, config client.TxEncodingConfig, message json.RawMessage) error { + var data GenesisState + err := marshaler.UnmarshalJSON(message, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// RegisterRESTRoutes registers the REST routes for the wasm module. +func (AppModuleBasic) RegisterRESTRoutes(cliCtx client.Context, rtr *mux.Router) { + rest.RegisterRoutes(cliCtx, rtr) +} + +// GetTxCmd returns the root tx command for the wasm module. +func (b AppModuleBasic) GetTxCmd() *cobra.Command { + return cli.GetTxCmd() +} + +// GetQueryCmd returns no root query command for the wasm module. +func (b AppModuleBasic) GetQueryCmd() *cobra.Command { + return cli.GetQueryCmd() +} + +// RegisterInterfaces implements InterfaceModule +func (b AppModuleBasic) RegisterInterfaces(registry cdctypes.InterfaceRegistry) { + types.RegisterInterfaces(registry) +} + +// ____________________________________________________________________________ + +// AppModule implements an application module for the wasm module. +type AppModule struct { + AppModuleBasic + cdc codec.Codec + keeper *Keeper + validatorSetSource keeper.ValidatorSetSource + accountKeeper types.AccountKeeper // for simulation + bankKeeper simulation.BankKeeper +} + +// ConsensusVersion is a sequence number for state-breaking change of the +// module. It should be incremented on each consensus-breaking change +// introduced by the module. To avoid wrong/empty versions, the initial version +// should be set to 1. +func (AppModule) ConsensusVersion() uint64 { return 2 } + +// NewAppModule creates a new AppModule object +func NewAppModule( + cdc codec.Codec, + keeper *Keeper, + validatorSetSource keeper.ValidatorSetSource, + ak types.AccountKeeper, + bk simulation.BankKeeper, +) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + cdc: cdc, + keeper: keeper, + validatorSetSource: validatorSetSource, + accountKeeper: ak, + bankKeeper: bk, + } +} + +func (am AppModule) RegisterServices(cfg module.Configurator) { + types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(keeper.NewDefaultPermissionKeeper(am.keeper))) + types.RegisterQueryServer(cfg.QueryServer(), NewQuerier(am.keeper)) + + m := keeper.NewMigrator(*am.keeper) + err := cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2) + if err != nil { + panic(err) + } +} + +func (am AppModule) LegacyQuerierHandler(amino *codec.LegacyAmino) sdk.Querier { //nolint:staticcheck + return keeper.NewLegacyQuerier(am.keeper, am.keeper.QueryGasLimit()) +} + +// RegisterInvariants registers the wasm module invariants. +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} + +// Route returns the message routing key for the wasm module. +func (am AppModule) Route() sdk.Route { + return sdk.NewRoute(RouterKey, NewHandler(keeper.NewDefaultPermissionKeeper(am.keeper))) +} + +// QuerierRoute returns the wasm module's querier route name. +func (AppModule) QuerierRoute() string { + return QuerierRoute +} + +// InitGenesis performs genesis initialization for the wasm module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + cdc.MustUnmarshalJSON(data, &genesisState) + validators, err := InitGenesis(ctx, am.keeper, genesisState) + if err != nil { + panic(err) + } + return validators +} + +// ExportGenesis returns the exported genesis state as raw bytes for the wasm +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return cdc.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{} +} + +// ____________________________________________________________________________ + +// AppModuleSimulation functions + +// GenerateGenesisState creates a randomized GenState of the bank module. +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// ProposalContents doesn't return any content functions for governance proposals. +func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { + return simulation.ProposalContents(am.bankKeeper, am.keeper) +} + +// RandomizedParams creates randomized bank param changes for the simulator. +func (am AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { + return simulation.ParamChanges(r, am.cdc) +} + +// RegisterStoreDecoder registers a decoder for supply module's types +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { +} + +// WeightedOperations returns the all the gov module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations(&simState, am.accountKeeper, am.bankKeeper, am.keeper) +} + +// ____________________________________________________________________________ + +// AddModuleInitFlags implements servertypes.ModuleInitFlags interface. +func AddModuleInitFlags(startCmd *cobra.Command) { + defaults := DefaultWasmConfig() + startCmd.Flags().Uint32(flagWasmMemoryCacheSize, defaults.MemoryCacheSize, "Sets the size in MiB (NOT bytes) of an in-memory cache for Wasm modules. Set to 0 to disable.") + startCmd.Flags().Uint64(flagWasmQueryGasLimit, defaults.SmartQueryGasLimit, "Set the max gas that can be spent on executing a query with a Wasm contract") + startCmd.Flags().String(flagWasmSimulationGasLimit, "", "Set the max gas that can be spent when executing a simulation TX") + + startCmd.PreRunE = chainPreRuns(checkLibwasmVersion, startCmd.PreRunE) +} + +// ReadWasmConfig reads the wasm specifig configuration +func ReadWasmConfig(opts servertypes.AppOptions) (types.WasmConfig, error) { + cfg := types.DefaultWasmConfig() + var err error + if v := opts.Get(flagWasmMemoryCacheSize); v != nil { + if cfg.MemoryCacheSize, err = cast.ToUint32E(v); err != nil { + return cfg, err + } + } + if v := opts.Get(flagWasmQueryGasLimit); v != nil { + if cfg.SmartQueryGasLimit, err = cast.ToUint64E(v); err != nil { + return cfg, err + } + } + if v := opts.Get(flagWasmSimulationGasLimit); v != nil { + if raw, ok := v.(string); ok && raw != "" { + limit, err := cast.ToUint64E(v) // non empty string set + if err != nil { + return cfg, err + } + cfg.SimulationGasLimit = &limit + } + } + // attach contract debugging to global "trace" flag + if v := opts.Get(server.FlagTrace); v != nil { + if cfg.ContractDebugMode, err = cast.ToBoolE(v); err != nil { + return cfg, err + } + } + return cfg, nil +} + +func getExpectedLibwasmVersion() string { + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + panic("can't read build info") + } + for _, d := range buildInfo.Deps { + if d.Path != "github.com/CosmWasm/wasmvm" { + continue + } + if d.Replace != nil { + return d.Replace.Version + } + return d.Version + } + return "" +} + +func checkLibwasmVersion(cmd *cobra.Command, args []string) error { + wasmVersion, err := wasmvm.LibwasmvmVersion() + if err != nil { + return fmt.Errorf("unable to retrieve libwasmversion %w", err) + } + wasmExpectedVersion := getExpectedLibwasmVersion() + if wasmExpectedVersion == "" { + return fmt.Errorf("wasmvm module not exist") + } + if !strings.Contains(wasmExpectedVersion, wasmVersion) { + return fmt.Errorf("libwasmversion mismatch. got: %s; expected: %s", wasmVersion, wasmExpectedVersion) + } + return nil +} + +type preRunFn func(cmd *cobra.Command, args []string) error + +func chainPreRuns(pfns ...preRunFn) preRunFn { + return func(cmd *cobra.Command, args []string) error { + for _, pfn := range pfns { + if pfn != nil { + if err := pfn(cmd, args); err != nil { + return err + } + } + } + return nil + } +} diff --git a/x/wasm/module_integration_test.go b/x/wasm/module_integration_test.go new file mode 100644 index 00000000..57f34324 --- /dev/null +++ b/x/wasm/module_integration_test.go @@ -0,0 +1,31 @@ +package wasm_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cerc-io/laconicd/app" + "github.com/cerc-io/laconicd/x/wasm" +) + +func TestModuleMigrations(t *testing.T) { + wasmApp := app.Setup(false) + ctx := wasmApp.BaseApp.NewContext(false, tmproto.Header{}) + upgradeHandler := func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + return wasmApp.ModuleManager().RunMigrations(ctx, wasmApp.ModuleConfigurator(), fromVM) + } + fromVM := wasmApp.UpgradeKeeper.GetModuleVersionMap(ctx) + fromVM[wasm.ModuleName] = 1 // start with initial version + upgradeHandler(ctx, upgradetypes.Plan{Name: "testing"}, fromVM) + // when + gotVM, err := wasmApp.ModuleManager().RunMigrations(ctx, wasmApp.ModuleConfigurator(), fromVM) + // then + require.NoError(t, err) + assert.Equal(t, uint64(2), gotVM[wasm.ModuleName]) +} diff --git a/x/wasm/module_test.go b/x/wasm/module_test.go new file mode 100644 index 00000000..ec9543d9 --- /dev/null +++ b/x/wasm/module_test.go @@ -0,0 +1,587 @@ +package wasm + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/cosmos/cosmos-sdk/types/module" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/dvsekhvalnov/jose2go/base64url" + "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" + + "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/keeper/testdata" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +type testData struct { + module module.AppModule + ctx sdk.Context + acctKeeper authkeeper.AccountKeeper + keeper Keeper + bankKeeper bankkeeper.Keeper + stakingKeeper stakingkeeper.Keeper + faucet *keeper.TestFaucet +} + +func setupTest(t *testing.T) testData { + ctx, keepers := CreateTestInput(t, false, "iterator,staking,stargate,cosmwasm_1_1") + cdc := keeper.MakeTestCodec(t) + data := testData{ + module: NewAppModule(cdc, keepers.WasmKeeper, keepers.StakingKeeper, keepers.AccountKeeper, keepers.BankKeeper), + ctx: ctx, + acctKeeper: keepers.AccountKeeper, + keeper: *keepers.WasmKeeper, + bankKeeper: keepers.BankKeeper, + stakingKeeper: keepers.StakingKeeper, + faucet: keepers.Faucet, + } + return data +} + +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 := os.ReadFile(path) + if err != nil { + panic(err) + } + return bz +} + +var ( + _, _, addrAcc1 = keyPubAddr() + addr1 = addrAcc1.String() + testContract = mustLoad("./keeper/testdata/hackatom.wasm") + maskContract = testdata.ReflectContractWasm() + oldContract = mustLoad("./testdata/escrow_0.7.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, + }, + "other valid wasm": { + msg: &MsgStoreCode{ + Sender: addr1, + WASMByteCode: maskContract, + }, + isValid: true, + }, + "old wasm (0.7)": { + msg: &MsgStoreCode{ + Sender: addr1, + WASMByteCode: oldContract, + }, + isValid: false, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + data := setupTest(t) + + h := data.module.Route().Handler() + q := data.module.LegacyQuerierHandler(nil) + + res, err := h(data.ctx, tc.msg) + if !tc.isValid { + require.Error(t, err, "%#v", res) + assertCodeList(t, q, data.ctx, 0) + assertCodeBytes(t, q, data.ctx, 1, nil) + return + } + require.NoError(t, err) + assertCodeList(t, q, data.ctx, 1) + }) + } +} + +type initMsg struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` +} + +type state struct { + Verifier string `json:"verifier"` + Beneficiary string `json:"beneficiary"` + Funder string `json:"funder"` +} + +func TestHandleInstantiate(t *testing.T) { + data := setupTest(t) + creator := data.faucet.NewFundedRandomAccount(data.ctx, sdk.NewInt64Coin("denom", 100000)) + + h := data.module.Route().Handler() + q := data.module.LegacyQuerierHandler(nil) + + msg := &MsgStoreCode{ + Sender: creator.String(), + WASMByteCode: testContract, + } + res, err := h(data.ctx, msg) + require.NoError(t, err) + assertStoreCodeResponse(t, res.Data, 1) + + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + + 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.String(), + CodeID: firstCodeID, + Msg: initMsgBz, + Funds: nil, + Label: "testing", + } + res, err = h(data.ctx, &initCmd) + require.NoError(t, err) + contractBech32Addr := parseInitResponse(t, res.Data) + + require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", contractBech32Addr) + // this should be standard x/wasm init event, nothing from contract + require.Equal(t, 3, len(res.Events), prettyEvents(res.Events)) + require.Equal(t, "message", res.Events[0].Type) + assertAttribute(t, "module", "wasm", res.Events[0].Attributes[0]) + require.Equal(t, "instantiate", res.Events[1].Type) + require.Equal(t, "wasm", res.Events[2].Type) + assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[2].Attributes[0]) + + assertCodeList(t, q, data.ctx, 1) + assertCodeBytes(t, q, data.ctx, 1, testContract) + + assertContractList(t, q, data.ctx, 1, []string{contractBech32Addr}) + assertContractInfo(t, q, data.ctx, contractBech32Addr, 1, creator) + assertContractState(t, q, data.ctx, contractBech32Addr, state{ + Verifier: fred.String(), + Beneficiary: bob.String(), + Funder: creator.String(), + }) +} + +func TestHandleExecute(t *testing.T) { + data := setupTest(t) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + + creator := data.faucet.NewFundedRandomAccount(data.ctx, deposit.Add(deposit...)...) + fred := data.faucet.NewFundedRandomAccount(data.ctx, topUp...) + + h := data.module.Route().Handler() + q := data.module.LegacyQuerierHandler(nil) + + msg := &MsgStoreCode{ + Sender: creator.String(), + WASMByteCode: testContract, + } + res, err := h(data.ctx, msg) + require.NoError(t, err) + assertStoreCodeResponse(t, res.Data, 1) + + _, _, bob := keyPubAddr() + initMsg := initMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator.String(), + CodeID: firstCodeID, + Msg: initMsgBz, + Funds: deposit, + Label: "testing", + } + res, err = h(data.ctx, &initCmd) + require.NoError(t, err) + contractBech32Addr := parseInitResponse(t, res.Data) + + require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", contractBech32Addr) + // this should be standard x/wasm message event, init event, plus a bank send event (2), with no custom contract events + require.Equal(t, 6, len(res.Events), prettyEvents(res.Events)) + require.Equal(t, "message", res.Events[0].Type) + assertAttribute(t, "module", "wasm", res.Events[0].Attributes[0]) + require.Equal(t, "coin_spent", res.Events[1].Type) + require.Equal(t, "coin_received", res.Events[2].Type) + require.Equal(t, "transfer", res.Events[3].Type) + require.Equal(t, "instantiate", res.Events[4].Type) + require.Equal(t, "wasm", res.Events[5].Type) + assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[5].Attributes[0]) + + // 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, data.bankKeeper.GetAllBalances(data.ctx, creatorAcct.GetAddress())) + + // ensure contract has updated balance + contractAddr, _ := sdk.AccAddressFromBech32(contractBech32Addr) + contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, deposit, data.bankKeeper.GetAllBalances(data.ctx, contractAcct.GetAddress())) + + execCmd := MsgExecuteContract{ + Sender: fred.String(), + Contract: contractBech32Addr, + Msg: []byte(`{"release":{}}`), + Funds: topUp, + } + res, err = h(data.ctx, &execCmd) + require.NoError(t, err) + // from https://github.com/CosmWasm/cosmwasm/blob/master/contracts/hackatom/src/contract.rs#L167 + assertExecuteResponse(t, res.Data, []byte{0xf0, 0x0b, 0xaa}) + + // this should be standard message event, plus x/wasm init event, plus 2 bank send event, plus a special event from the contract + require.Equal(t, 10, len(res.Events), prettyEvents(res.Events)) + + assert.Equal(t, "message", res.Events[0].Type) + assertAttribute(t, "module", "wasm", res.Events[0].Attributes[0]) + assert.Equal(t, "coin_spent", res.Events[1].Type) + assert.Equal(t, "coin_received", res.Events[2].Type) + + require.Equal(t, "transfer", res.Events[3].Type) + require.Len(t, res.Events[3].Attributes, 3) + assertAttribute(t, "recipient", contractBech32Addr, res.Events[3].Attributes[0]) + assertAttribute(t, "sender", fred.String(), res.Events[3].Attributes[1]) + assertAttribute(t, "amount", "5000denom", res.Events[3].Attributes[2]) + + assert.Equal(t, "execute", res.Events[4].Type) + + // custom contract event attribute + assert.Equal(t, "wasm", res.Events[5].Type) + assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[5].Attributes[0]) + assertAttribute(t, "action", "release", res.Events[5].Attributes[1]) + // custom contract event + assert.Equal(t, "wasm-hackatom", res.Events[6].Type) + assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[6].Attributes[0]) + assertAttribute(t, "action", "release", res.Events[6].Attributes[1]) + // second transfer (this without conflicting message) + assert.Equal(t, "coin_spent", res.Events[7].Type) + assert.Equal(t, "coin_received", res.Events[8].Type) + + assert.Equal(t, "transfer", res.Events[9].Type) + assertAttribute(t, "recipient", bob.String(), res.Events[9].Attributes[0]) + assertAttribute(t, "sender", contractBech32Addr, res.Events[9].Attributes[1]) + assertAttribute(t, "amount", "105000denom", res.Events[9].Attributes[2]) + // finally, standard x/wasm tag + + // ensure bob now exists and got both payments released + bobAcct = data.acctKeeper.GetAccount(data.ctx, bob) + require.NotNil(t, bobAcct) + balance := data.bankKeeper.GetAllBalances(data.ctx, bobAcct.GetAddress()) + 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{}, data.bankKeeper.GetAllBalances(data.ctx, contractAcct.GetAddress())) + + // 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, 1, []string{contractBech32Addr}) + assertContractInfo(t, q, data.ctx, contractBech32Addr, 1, creator) + assertContractState(t, q, data.ctx, contractBech32Addr, state{ + Verifier: fred.String(), + Beneficiary: bob.String(), + Funder: creator.String(), + }) +} + +func TestHandleExecuteEscrow(t *testing.T) { + data := setupTest(t) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := sdk.AccAddress(bytes.Repeat([]byte{1}, address.Len)) + data.faucet.Fund(data.ctx, creator, sdk.NewInt64Coin("denom", 100000)) + fred := data.faucet.NewFundedRandomAccount(data.ctx, topUp...) + + h := data.module.Route().Handler() + + msg := &MsgStoreCode{ + Sender: creator.String(), + WASMByteCode: testContract, + } + res, err := h(data.ctx, msg) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := map[string]interface{}{ + "verifier": fred.String(), + "beneficiary": bob.String(), + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator.String(), + CodeID: firstCodeID, + Msg: initMsgBz, + Funds: deposit, + Label: "testing", + } + res, err = h(data.ctx, &initCmd) + require.NoError(t, err) + contractBech32Addr := parseInitResponse(t, res.Data) + require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", contractBech32Addr) + + handleMsg := map[string]interface{}{ + "release": map[string]interface{}{}, + } + handleMsgBz, err := json.Marshal(handleMsg) + require.NoError(t, err) + + execCmd := MsgExecuteContract{ + Sender: fred.String(), + Contract: contractBech32Addr, + Msg: handleMsgBz, + Funds: topUp, + } + res, err = h(data.ctx, &execCmd) + require.NoError(t, err) + // from https://github.com/CosmWasm/cosmwasm/blob/master/contracts/hackatom/src/contract.rs#L167 + assertExecuteResponse(t, res.Data, []byte{0xf0, 0x0b, 0xaa}) + + // ensure bob now exists and got both payments released + bobAcct := data.acctKeeper.GetAccount(data.ctx, bob) + require.NotNil(t, bobAcct) + balance := data.bankKeeper.GetAllBalances(data.ctx, bobAcct.GetAddress()) + assert.Equal(t, deposit.Add(topUp...), balance) + + // ensure contract has updated balance + contractAddr, _ := sdk.AccAddressFromBech32(contractBech32Addr) + contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, sdk.Coins{}, data.bankKeeper.GetAllBalances(data.ctx, contractAcct.GetAddress())) +} + +func TestReadWasmConfig(t *testing.T) { + defaults := DefaultWasmConfig() + specs := map[string]struct { + src AppOptionsMock + exp types.WasmConfig + }{ + "set query gas limit via opts": { + src: AppOptionsMock{ + "wasm.query_gas_limit": 1, + }, + exp: types.WasmConfig{ + SmartQueryGasLimit: 1, + MemoryCacheSize: defaults.MemoryCacheSize, + }, + }, + "set cache via opts": { + src: AppOptionsMock{ + "wasm.memory_cache_size": 2, + }, + exp: types.WasmConfig{ + MemoryCacheSize: 2, + SmartQueryGasLimit: defaults.SmartQueryGasLimit, + }, + }, + "set debug via opts": { + src: AppOptionsMock{ + "trace": true, + }, + exp: types.WasmConfig{ + SmartQueryGasLimit: defaults.SmartQueryGasLimit, + MemoryCacheSize: defaults.MemoryCacheSize, + ContractDebugMode: true, + }, + }, + "all defaults when no options set": { + exp: defaults, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := ReadWasmConfig(spec.src) + require.NoError(t, err) + assert.Equal(t, spec.exp, got) + }) + } +} + +type AppOptionsMock map[string]interface{} + +func (a AppOptionsMock) Get(s string) interface{} { + return a[s] +} + +type prettyEvent struct { + Type string + Attr []sdk.Attribute +} + +func prettyEvents(evts []abci.Event) string { + res := make([]prettyEvent, len(evts)) + for i, e := range evts { + res[i] = prettyEvent{ + Type: e.Type, + Attr: prettyAttrs(e.Attributes), + } + } + bz, err := json.MarshalIndent(res, "", " ") + if err != nil { + panic(err) + } + return string(bz) +} + +func prettyAttrs(attrs []abci.EventAttribute) []sdk.Attribute { + pretty := make([]sdk.Attribute, len(attrs)) + for i, a := range attrs { + pretty[i] = prettyAttr(a) + } + return pretty +} + +func prettyAttr(attr abci.EventAttribute) sdk.Attribute { + return sdk.NewAttribute(string(attr.Key), string(attr.Value)) +} + +func assertAttribute(t *testing.T, key string, value string, attr abci.EventAttribute) { + t.Helper() + assert.Equal(t, key, string(attr.Key), prettyAttr(attr)) + assert.Equal(t, value, string(attr.Value), prettyAttr(attr)) +} + +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)) +} + +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(expectedBytes) == 0 { + require.Equal(t, len(bz), 0, "%q", string(bz)) + return + } + var res map[string]interface{} + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + require.Contains(t, res, "data") + b, err := base64url.Decode(res["data"].(string)) + require.NoError(t, err) + assert.Equal(t, expectedBytes, b) + assert.EqualValues(t, codeID, res["id"]) +} + +func assertContractList(t *testing.T, q sdk.Querier, ctx sdk.Context, codeID uint64, expContractAddrs []string) { + bz, sdkerr := q(ctx, []string{QueryListContractByCode, fmt.Sprintf("%d", codeID)}, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, len(expContractAddrs), 0) + return + } + + var res []string + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + hasAddrs := make([]string, len(res)) + for i, r := range res { + hasAddrs[i] = r + } + + assert.Equal(t, expContractAddrs, hasAddrs) +} + +func assertContractState(t *testing.T, q sdk.Querier, ctx sdk.Context, contractBech32Addr string, expected state) { + t.Helper() + path := []string{QueryGetContractState, contractBech32Addr, keeper.QueryMethodContractStateAll} + 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, []byte("config"), []byte(res[0].Key)) + + expectedBz, err := json.Marshal(expected) + require.NoError(t, err) + assert.Equal(t, expectedBz, res[0].Value) +} + +func assertContractInfo(t *testing.T, q sdk.Querier, ctx sdk.Context, contractBech32Addr string, codeID uint64, creator sdk.AccAddress) { + t.Helper() + path := []string{QueryGetContract, contractBech32Addr} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + var res ContractInfo + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + assert.Equal(t, codeID, res.CodeID) + assert.Equal(t, creator.String(), res.Creator) +} diff --git a/x/wasm/relay_pingpong_test.go b/x/wasm/relay_pingpong_test.go new file mode 100644 index 00000000..13fef9a3 --- /dev/null +++ b/x/wasm/relay_pingpong_test.go @@ -0,0 +1,396 @@ +package wasm_test + +import ( + "encoding/json" + "fmt" + "testing" + + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + wasmibctesting "github.com/cerc-io/laconicd/x/wasm/ibctesting" + wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + wasmtypes "github.com/cerc-io/laconicd/x/wasm/types" +) + +const ( + ping = "ping" + pong = "pong" +) + +var doNotTimeout = clienttypes.NewHeight(1, 1111111) + +func TestPinPong(t *testing.T) { + // custom IBC protocol example + // scenario: given two chains, + // with a contract on chain A and chain B + // when a ibc packet comes in, the contract responds with a new packet containing + // either ping or pong + + pingContract := &player{t: t, actor: ping} + pongContract := &player{t: t, actor: pong} + + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(pingContract)), + } + chainBOpts = []wasmkeeper.Option{wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(pongContract), + )} + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts, chainBOpts) + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + _ = chainB.SeedNewContractInstance() // skip 1 instance so that addresses are not the same + var ( + pingContractAddr = chainA.SeedNewContractInstance() + pongContractAddr = chainB.SeedNewContractInstance() + ) + require.NotEqual(t, pingContractAddr, pongContractAddr) + coordinator.CommitBlock(chainA, chainB) + + pingContract.chain = chainA + pingContract.contractAddr = pingContractAddr + + pongContract.chain = chainB + pongContract.contractAddr = pongContractAddr + + var ( + sourcePortID = wasmkeeper.PortIDForContract(pingContractAddr) + counterpartyPortID = wasmkeeper.PortIDForContract(pongContractAddr) + ) + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: sourcePortID, + Version: ibctransfertypes.Version, + Order: channeltypes.ORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: counterpartyPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.ORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + // trigger start game via execute + const startValue uint64 = 100 + const rounds = 3 + s := startGame{ + ChannelID: path.EndpointA.ChannelID, + Value: startValue, + } + startMsg := &wasmtypes.MsgExecuteContract{ + Sender: chainA.SenderAccount.GetAddress().String(), + Contract: pingContractAddr.String(), + Msg: s.GetBytes(), + } + // on chain A + _, err := path.EndpointA.Chain.SendMsgs(startMsg) + require.NoError(t, err) + + // when some rounds are played + for i := 1; i <= rounds; i++ { + t.Logf("++ round: %d\n", i) + + require.Len(t, chainA.PendingSendPackets, 1) + err := coordinator.RelayAndAckPendingPackets(path) + require.NoError(t, err) + } + + // then receive/response state is as expected + assert.Equal(t, startValue+rounds, pingContract.QueryState(lastBallSentKey)) + assert.Equal(t, uint64(rounds), pingContract.QueryState(lastBallReceivedKey)) + assert.Equal(t, uint64(rounds+1), pingContract.QueryState(sentBallsCountKey)) + assert.Equal(t, uint64(rounds), pingContract.QueryState(receivedBallsCountKey)) + assert.Equal(t, uint64(rounds), pingContract.QueryState(confirmedBallsCountKey)) + + assert.Equal(t, uint64(rounds), pongContract.QueryState(lastBallSentKey)) + assert.Equal(t, startValue+rounds-1, pongContract.QueryState(lastBallReceivedKey)) + assert.Equal(t, uint64(rounds), pongContract.QueryState(sentBallsCountKey)) + assert.Equal(t, uint64(rounds), pongContract.QueryState(receivedBallsCountKey)) + assert.Equal(t, uint64(rounds), pongContract.QueryState(confirmedBallsCountKey)) +} + +var _ wasmtesting.IBCContractCallbacks = &player{} + +// player is a (mock) contract that sends and receives ibc packages +type player struct { + t *testing.T + chain *wasmibctesting.TestChain + contractAddr sdk.AccAddress + actor string // either ping or pong + execCalls int // number of calls to Execute method (checkTx + deliverTx) +} + +// Execute starts the ping pong game +// Contracts finds all connected channels and broadcasts a ping message +func (p *player) Execute(code wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + p.execCalls++ + // start game + var start startGame + if err := json.Unmarshal(executeMsg, &start); err != nil { + return nil, 0, err + } + + if start.MaxValue != 0 { + store.Set(maxValueKey, sdk.Uint64ToBigEndian(start.MaxValue)) + } + service := NewHit(p.actor, start.Value) + p.t.Logf("[%s] starting game with: %d: %v\n", p.actor, start.Value, service) + + p.incrementCounter(sentBallsCountKey, store) + store.Set(lastBallSentKey, sdk.Uint64ToBigEndian(start.Value)) + return &wasmvmtypes.Response{ + Messages: []wasmvmtypes.SubMsg{ + { + Msg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + SendPacket: &wasmvmtypes.SendPacketMsg{ + ChannelID: start.ChannelID, + Data: service.GetBytes(), + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{ + Revision: doNotTimeout.RevisionNumber, + Height: doNotTimeout.RevisionHeight, + }}, + }, + }, + }, + ReplyOn: wasmvmtypes.ReplyNever, + }, + }, + }, 0, nil +} + +// OnIBCChannelOpen ensures to accept only configured version +func (p player) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + if msg.GetChannel().Version != p.actor { + return &wasmvmtypes.IBC3ChannelOpenResponse{}, 0, nil + } + return &wasmvmtypes.IBC3ChannelOpenResponse{}, 0, nil +} + +// OnIBCChannelConnect persists connection endpoints +func (p player) IBCChannelConnect(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + p.storeEndpoint(store, msg.GetChannel()) + return &wasmvmtypes.IBCBasicResponse{}, 0, nil +} + +// connectedChannelsModel is a simple persistence model to store endpoint addresses within the contract's store +type connectedChannelsModel struct { + Our wasmvmtypes.IBCEndpoint + Their wasmvmtypes.IBCEndpoint +} + +var ( // store keys + ibcEndpointsKey = []byte("ibc-endpoints") + maxValueKey = []byte("max-value") +) + +func (p player) loadEndpoints(store prefix.Store, channelID string) *connectedChannelsModel { + var counterparties []connectedChannelsModel + if bz := store.Get(ibcEndpointsKey); bz != nil { + require.NoError(p.t, json.Unmarshal(bz, &counterparties)) + } + for _, v := range counterparties { + if v.Our.ChannelID == channelID { + return &v + } + } + p.t.Fatalf("no counterparty found for channel %q", channelID) + return nil +} + +func (p player) storeEndpoint(store wasmvm.KVStore, channel wasmvmtypes.IBCChannel) { + var counterparties []connectedChannelsModel + if b := store.Get(ibcEndpointsKey); b != nil { + require.NoError(p.t, json.Unmarshal(b, &counterparties)) + } + counterparties = append(counterparties, connectedChannelsModel{Our: channel.Endpoint, Their: channel.CounterpartyEndpoint}) + bz, err := json.Marshal(&counterparties) + require.NoError(p.t, err) + store.Set(ibcEndpointsKey, bz) +} + +func (p player) IBCChannelClose(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + panic("implement me") +} + +var ( // store keys + lastBallSentKey = []byte("lastBallSent") + lastBallReceivedKey = []byte("lastBallReceived") + sentBallsCountKey = []byte("sentBalls") + receivedBallsCountKey = []byte("recvBalls") + confirmedBallsCountKey = []byte("confBalls") +) + +// IBCPacketReceive receives the hit and serves a response hit via `wasmvmtypes.IBCPacket` +func (p player) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + // parse received data and store + packet := msg.Packet + var receivedBall hit + if err := json.Unmarshal(packet.Data, &receivedBall); err != nil { + return &wasmvmtypes.IBCReceiveResult{ + Ok: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: hitAcknowledgement{Error: err.Error()}.GetBytes(), + }, + // no hit msg, we stop the game + }, 0, nil + } + p.incrementCounter(receivedBallsCountKey, store) + + otherCount := receivedBall[counterParty(p.actor)] + store.Set(lastBallReceivedKey, sdk.Uint64ToBigEndian(otherCount)) + + if maxVal := store.Get(maxValueKey); maxVal != nil && otherCount > sdk.BigEndianToUint64(maxVal) { + errMsg := fmt.Sprintf("max value exceeded: %d got %d", sdk.BigEndianToUint64(maxVal), otherCount) + return &wasmvmtypes.IBCReceiveResult{Ok: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: receivedBall.BuildError(errMsg).GetBytes(), + }}, 0, nil + } + + nextValue := p.incrementCounter(lastBallSentKey, store) + newHit := NewHit(p.actor, nextValue) + respHit := &wasmvmtypes.IBCMsg{SendPacket: &wasmvmtypes.SendPacketMsg{ + ChannelID: packet.Src.ChannelID, + Data: newHit.GetBytes(), + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{ + Revision: doNotTimeout.RevisionNumber, + Height: doNotTimeout.RevisionHeight, + }}, + }} + p.incrementCounter(sentBallsCountKey, store) + p.t.Logf("[%s] received %d, returning %d: %v\n", p.actor, otherCount, nextValue, newHit) + + return &wasmvmtypes.IBCReceiveResult{ + Ok: &wasmvmtypes.IBCReceiveResponse{ + Acknowledgement: receivedBall.BuildAck().GetBytes(), + Messages: []wasmvmtypes.SubMsg{{Msg: wasmvmtypes.CosmosMsg{IBC: respHit}, ReplyOn: wasmvmtypes.ReplyNever}}, + }, + }, 0, nil +} + +// OnIBCPacketAcknowledgement handles the packet acknowledgment frame. Stops the game on an any error +func (p player) IBCPacketAck(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + // parse received data and store + var sentBall hit + if err := json.Unmarshal(msg.OriginalPacket.Data, &sentBall); err != nil { + return nil, 0, err + } + + var ack hitAcknowledgement + if err := json.Unmarshal(msg.Acknowledgement.Data, &ack); err != nil { + return nil, 0, err + } + if ack.Success != nil { + confirmedCount := sentBall[p.actor] + p.t.Logf("[%s] acknowledged %d: %v\n", p.actor, confirmedCount, sentBall) + } else { + p.t.Logf("[%s] received app layer error: %s\n", p.actor, ack.Error) + } + + p.incrementCounter(confirmedBallsCountKey, store) + return &wasmvmtypes.IBCBasicResponse{}, 0, nil +} + +func (p player) IBCPacketTimeout(codeID wasmvm.Checksum, env wasmvmtypes.Env, packet wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + panic("implement me") +} + +func (p player) incrementCounter(key []byte, store wasmvm.KVStore) uint64 { + var count uint64 + bz := store.Get(key) + if bz != nil { + count = sdk.BigEndianToUint64(bz) + } + count++ + store.Set(key, sdk.Uint64ToBigEndian(count)) + return count +} + +func (p player) QueryState(key []byte) uint64 { + raw := p.chain.App.WasmKeeper.QueryRaw(p.chain.GetContext(), p.contractAddr, key) + return sdk.BigEndianToUint64(raw) +} + +func counterParty(s string) string { + switch s { + case ping: + return pong + case pong: + return ping + default: + panic(fmt.Sprintf("unsupported: %q", s)) + } +} + +// hit is ibc packet payload +type hit map[string]uint64 + +func NewHit(player string, count uint64) hit { + return map[string]uint64{ + player: count, + } +} + +func (h hit) GetBytes() []byte { + b, err := json.Marshal(h) + if err != nil { + panic(err) + } + return b +} + +func (h hit) String() string { + return fmt.Sprintf("Ball %s", string(h.GetBytes())) +} + +func (h hit) BuildAck() hitAcknowledgement { + return hitAcknowledgement{Success: &h} +} + +func (h hit) BuildError(errMsg string) hitAcknowledgement { + return hitAcknowledgement{Error: errMsg} +} + +// hitAcknowledgement is ibc acknowledgment payload +type hitAcknowledgement struct { + Error string `json:"error,omitempty"` + Success *hit `json:"success,omitempty"` +} + +func (a hitAcknowledgement) GetBytes() []byte { + b, err := json.Marshal(a) + if err != nil { + panic(err) + } + return b +} + +// startGame is an execute message payload +type startGame struct { + ChannelID string + Value uint64 + // limit above the game is aborted + MaxValue uint64 `json:"max_value,omitempty"` +} + +func (g startGame) GetBytes() wasmtypes.RawContractMessage { + b, err := json.Marshal(g) + if err != nil { + panic(err) + } + return b +} diff --git a/x/wasm/relay_test.go b/x/wasm/relay_test.go new file mode 100644 index 00000000..aff1df5a --- /dev/null +++ b/x/wasm/relay_test.go @@ -0,0 +1,808 @@ +package wasm_test + +import ( + "encoding/json" + "errors" + "testing" + "time" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + wasmibctesting "github.com/cerc-io/laconicd/x/wasm/ibctesting" + wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper" + wasmtesting "github.com/cerc-io/laconicd/x/wasm/keeper/wasmtesting" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func TestFromIBCTransferToContract(t *testing.T) { + // scenario: given two chains, + // with a contract on chain B + // then the contract can handle the receiving side of an ics20 transfer + // that was started on chain A via ibc transfer module + + transferAmount := sdk.NewInt(1) + specs := map[string]struct { + contract wasmtesting.IBCContractCallbacks + setupContract func(t *testing.T, contract wasmtesting.IBCContractCallbacks, chain *wasmibctesting.TestChain) + expChainABalanceDiff sdk.Int + expChainBBalanceDiff sdk.Int + }{ + "ack": { + contract: &ackReceiverContract{}, + setupContract: func(t *testing.T, contract wasmtesting.IBCContractCallbacks, chain *wasmibctesting.TestChain) { + c := contract.(*ackReceiverContract) + c.t = t + c.chain = chain + }, + expChainABalanceDiff: transferAmount.Neg(), + expChainBBalanceDiff: transferAmount, + }, + "nack": { + contract: &nackReceiverContract{}, + setupContract: func(t *testing.T, contract wasmtesting.IBCContractCallbacks, chain *wasmibctesting.TestChain) { + c := contract.(*nackReceiverContract) + c.t = t + }, + expChainABalanceDiff: sdk.ZeroInt(), + expChainBBalanceDiff: sdk.ZeroInt(), + }, + "error": { + contract: &errorReceiverContract{}, + setupContract: func(t *testing.T, contract wasmtesting.IBCContractCallbacks, chain *wasmibctesting.TestChain) { + c := contract.(*errorReceiverContract) + c.t = t + }, + expChainABalanceDiff: sdk.ZeroInt(), + expChainBBalanceDiff: sdk.ZeroInt(), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var ( + chainAOpts = []wasmkeeper.Option{wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(spec.contract), + )} + coordinator = wasmibctesting.NewCoordinator(t, 2, []wasmkeeper.Option{}, chainAOpts) + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + coordinator.CommitBlock(chainA, chainB) + myContractAddr := chainB.SeedNewContractInstance() + contractBPortID := chainB.ContractInfo(myContractAddr).IBCPortID + + spec.setupContract(t, spec.contract, chainB) + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: "transfer", + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: contractBPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + originalChainABalance := chainA.Balance(chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + // when transfer via sdk transfer from A (module) -> B (contract) + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, transferAmount) + timeoutHeight := clienttypes.NewHeight(1, 110) + msg := ibctransfertypes.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, coinToSendToB, chainA.SenderAccount.GetAddress().String(), chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0) + _, err := chainA.SendMsgs(msg) + require.NoError(t, err) + require.NoError(t, path.EndpointB.UpdateClient()) + + // then + require.Equal(t, 1, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and when relay to chain B and handle Ack on chain A + err = coordinator.RelayAndAckPendingPackets(path) + require.NoError(t, err) + + // then + require.Equal(t, 0, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and source chain balance was decreased + newChainABalance := chainA.Balance(chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + assert.Equal(t, originalChainABalance.Amount.Add(spec.expChainABalanceDiff), newChainABalance.Amount) + + // and dest chain balance contains voucher + expBalance := ibctransfertypes.GetTransferCoin(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, coinToSendToB.Denom, spec.expChainBBalanceDiff) + gotBalance := chainB.Balance(chainB.SenderAccount.GetAddress(), expBalance.Denom) + assert.Equal(t, expBalance, gotBalance, "got total balance: %s", chainB.AllBalances(chainB.SenderAccount.GetAddress())) + }) + } +} + +func TestContractCanInitiateIBCTransferMsg(t *testing.T) { + // scenario: given two chains, + // with a contract on chain A + // then the contract can start an ibc transfer via ibctransfertypes.NewMsgTransfer + // that is handled on chain A by the ibc transfer module and + // received on chain B via ibc transfer module as well + + myContract := &sendViaIBCTransferContract{t: t} + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContract)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts) + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + myContractAddr := chainA.SeedNewContractInstance() + coordinator.CommitBlock(chainA, chainB) + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: ibctransfertypes.PortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: ibctransfertypes.PortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + // when contract is triggered to send IBCTransferMsg + receiverAddress := chainB.SenderAccount.GetAddress() + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) + + // start transfer from chainA to chainB + startMsg := &types.MsgExecuteContract{ + Sender: chainA.SenderAccount.GetAddress().String(), + Contract: myContractAddr.String(), + Msg: startTransfer{ + ChannelID: path.EndpointA.ChannelID, + CoinsToSend: coinToSendToB, + ReceiverAddr: receiverAddress.String(), + }.GetBytes(), + } + _, err := chainA.SendMsgs(startMsg) + require.NoError(t, err) + + // then + require.Equal(t, 1, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and when relay to chain B and handle Ack on chain A + err = coordinator.RelayAndAckPendingPackets(path) + require.NoError(t, err) + + // then + require.Equal(t, 0, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and dest chain balance contains voucher + bankKeeperB := chainB.App.BankKeeper + expBalance := ibctransfertypes.GetTransferCoin(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, coinToSendToB.Denom, coinToSendToB.Amount) + gotBalance := chainB.Balance(chainB.SenderAccount.GetAddress(), expBalance.Denom) + assert.Equal(t, expBalance, gotBalance, "got total balance: %s", bankKeeperB.GetAllBalances(chainB.GetContext(), chainB.SenderAccount.GetAddress())) +} + +func TestContractCanEmulateIBCTransferMessage(t *testing.T) { + // scenario: given two chains, + // with a contract on chain A + // then the contract can emulate the ibc transfer module in the contract to send an ibc packet + // which is received on chain B via ibc transfer module + + myContract := &sendEmulatedIBCTransferContract{t: t} + + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContract)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts) + + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + myContractAddr := chainA.SeedNewContractInstance() + myContract.contractAddr = myContractAddr.String() + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainA.ContractInfo(myContractAddr).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: ibctransfertypes.PortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + // when contract is triggered to send the ibc package to chain B + timeout := uint64(chainB.LastHeader.Header.Time.Add(time.Hour).UnixNano()) // enough time to not timeout + receiverAddress := chainB.SenderAccount.GetAddress() + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) + + // start transfer from chainA to chainB + startMsg := &types.MsgExecuteContract{ + Sender: chainA.SenderAccount.GetAddress().String(), + Contract: myContractAddr.String(), + Msg: startTransfer{ + ChannelID: path.EndpointA.ChannelID, + CoinsToSend: coinToSendToB, + ReceiverAddr: receiverAddress.String(), + ContractIBCPort: chainA.ContractInfo(myContractAddr).IBCPortID, + Timeout: timeout, + }.GetBytes(), + Funds: sdk.NewCoins(coinToSendToB), + } + _, err := chainA.SendMsgs(startMsg) + require.NoError(t, err) + + // then + require.Equal(t, 1, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and when relay to chain B and handle Ack on chain A + err = coordinator.RelayAndAckPendingPackets(path) + require.NoError(t, err) + + // then + require.Equal(t, 0, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and dest chain balance contains voucher + bankKeeperB := chainB.App.BankKeeper + expBalance := ibctransfertypes.GetTransferCoin(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, coinToSendToB.Denom, coinToSendToB.Amount) + gotBalance := chainB.Balance(chainB.SenderAccount.GetAddress(), expBalance.Denom) + assert.Equal(t, expBalance, gotBalance, "got total balance: %s", bankKeeperB.GetAllBalances(chainB.GetContext(), chainB.SenderAccount.GetAddress())) +} + +func TestContractCanEmulateIBCTransferMessageWithTimeout(t *testing.T) { + // scenario: given two chains, + // with a contract on chain A + // then the contract can emulate the ibc transfer module in the contract to send an ibc packet + // which is not received on chain B and times out + + myContract := &sendEmulatedIBCTransferContract{t: t} + + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContract)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts) + + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + coordinator.CommitBlock(chainA, chainB) + myContractAddr := chainA.SeedNewContractInstance() + myContract.contractAddr = myContractAddr.String() + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainA.ContractInfo(myContractAddr).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: ibctransfertypes.PortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + coordinator.UpdateTime() + + // when contract is triggered to send the ibc package to chain B + timeout := uint64(chainB.LastHeader.Header.Time.Add(time.Nanosecond).UnixNano()) // will timeout + receiverAddress := chainB.SenderAccount.GetAddress() + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) + initialContractBalance := chainA.Balance(myContractAddr, sdk.DefaultBondDenom) + initialSenderBalance := chainA.Balance(chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + + // custom payload data to be transferred into a proper ICS20 ibc packet + startMsg := &types.MsgExecuteContract{ + Sender: chainA.SenderAccount.GetAddress().String(), + Contract: myContractAddr.String(), + Msg: startTransfer{ + ChannelID: path.EndpointA.ChannelID, + CoinsToSend: coinToSendToB, + ReceiverAddr: receiverAddress.String(), + ContractIBCPort: chainA.ContractInfo(myContractAddr).IBCPortID, + Timeout: timeout, + }.GetBytes(), + Funds: sdk.NewCoins(coinToSendToB), + } + _, err := chainA.SendMsgs(startMsg) + require.NoError(t, err) + coordinator.CommitBlock(chainA, chainB) + // then + require.Equal(t, 1, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + newContractBalance := chainA.Balance(myContractAddr, sdk.DefaultBondDenom) + assert.Equal(t, initialContractBalance.Add(coinToSendToB), newContractBalance) // hold in escrow + + // when timeout packet send (by the relayer) + err = coordinator.TimeoutPendingPackets(path) + require.NoError(t, err) + coordinator.CommitBlock(chainA) + + // then + require.Equal(t, 0, len(chainA.PendingSendPackets)) + require.Equal(t, 0, len(chainB.PendingSendPackets)) + + // and then verify account balances restored + newContractBalance = chainA.Balance(myContractAddr, sdk.DefaultBondDenom) + assert.Equal(t, initialContractBalance.String(), newContractBalance.String()) + newSenderBalance := chainA.Balance(chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + assert.Equal(t, initialSenderBalance.String(), newSenderBalance.String()) +} + +func TestContractEmulateIBCTransferMessageOnDiffContractIBCChannel(t *testing.T) { + // scenario: given two chains, A and B + // with 2 contract A1 and A2 on chain A + // then the contract A2 try to send an ibc packet via IBC Channel that create by A1 and B + myContractA1 := &sendEmulatedIBCTransferContract{} + myContractA2 := &sendEmulatedIBCTransferContract{} + + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractA1), + ), + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractA2), + ), + } + + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts) + + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + + coordinator.CommitBlock(chainA, chainB) + myContractAddr1 := chainA.SeedNewContractInstance() + myContractA1.contractAddr = myContractAddr1.String() + myContractAddr2 := chainA.SeedNewContractInstance() + myContractA2.contractAddr = myContractAddr2.String() + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainA.ContractInfo(myContractAddr1).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: ibctransfertypes.PortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + // when contract is triggered to send the ibc package to chain B + timeout := uint64(chainB.LastHeader.Header.Time.Add(time.Hour).UnixNano()) // enough time to not timeout + receiverAddress := chainB.SenderAccount.GetAddress() + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) + + // start transfer from chainA - A2 to chainB via IBC channel + startMsg := &types.MsgExecuteContract{ + Sender: chainA.SenderAccount.GetAddress().String(), + Contract: myContractAddr2.String(), + Msg: startTransfer{ + ChannelID: path.EndpointA.ChannelID, + CoinsToSend: coinToSendToB, + ReceiverAddr: receiverAddress.String(), + Timeout: timeout, + }.GetBytes(), + Funds: sdk.NewCoins(coinToSendToB), + } + _, err := chainA.SendMsgs(startMsg) + require.Error(t, err) +} + +func TestContractHandlesChannelClose(t *testing.T) { + // scenario: a contract is the sending side of an ics20 transfer but the packet was not received + // on the destination chain within the timeout boundaries + myContractA := &captureCloseContract{} + myContractB := &captureCloseContract{} + + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractA)), + } + chainBOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractB)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts, chainBOpts) + + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + + coordinator.CommitBlock(chainA, chainB) + myContractAddrA := chainA.SeedNewContractInstance() + _ = chainB.SeedNewContractInstance() // skip one instance + myContractAddrB := chainB.SeedNewContractInstance() + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainA.ContractInfo(myContractAddrA).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainB.ContractInfo(myContractAddrB).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + coordinator.CloseChannel(path) + assert.True(t, myContractB.closeCalled) +} + +func TestContractHandlesChannelCloseNotOwned(t *testing.T) { + // scenario: given two chains, + // with a contract A1, A2 on chain A, contract B on chain B + // contract A2 try to close ibc channel that create between A1 and B + + myContractA1 := &closeChannelContract{} + myContractA2 := &closeChannelContract{} + myContractB := &closeChannelContract{} + + var ( + chainAOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractA1)), + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractA2)), + } + chainBOpts = []wasmkeeper.Option{ + wasmkeeper.WithWasmEngine( + wasmtesting.NewIBCContractMockWasmer(myContractB)), + } + coordinator = wasmibctesting.NewCoordinator(t, 2, chainAOpts, chainBOpts) + + chainA = coordinator.GetChain(wasmibctesting.GetChainID(0)) + chainB = coordinator.GetChain(wasmibctesting.GetChainID(1)) + ) + + coordinator.CommitBlock(chainA, chainB) + myContractAddrA1 := chainA.SeedNewContractInstance() + myContractAddrA2 := chainA.SeedNewContractInstance() + _ = chainB.SeedNewContractInstance() // skip one instance + _ = chainB.SeedNewContractInstance() // skip one instance + myContractAddrB := chainB.SeedNewContractInstance() + + path := wasmibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainA.ContractInfo(myContractAddrA1).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ + PortID: chainB.ContractInfo(myContractAddrB).IBCPortID, + Version: ibctransfertypes.Version, + Order: channeltypes.UNORDERED, + } + coordinator.SetupConnections(path) + coordinator.CreateChannels(path) + + closeIBCChannelMsg := &types.MsgExecuteContract{ + Sender: chainA.SenderAccount.GetAddress().String(), + Contract: myContractAddrA2.String(), + Msg: closeIBCChannel{ + ChannelID: path.EndpointA.ChannelID, + }.GetBytes(), + Funds: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))), + } + + _, err := chainA.SendMsgs(closeIBCChannelMsg) + require.Error(t, err) +} + +var _ wasmtesting.IBCContractCallbacks = &captureCloseContract{} + +// contract that sets a flag on IBC channel close only. +type captureCloseContract struct { + contractStub + closeCalled bool +} + +func (c *captureCloseContract) IBCChannelClose(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + c.closeCalled = true + return &wasmvmtypes.IBCBasicResponse{}, 1, nil +} + +var _ wasmtesting.IBCContractCallbacks = &sendViaIBCTransferContract{} + +// contract that initiates an ics-20 transfer on execute via sdk message +type sendViaIBCTransferContract struct { + contractStub + t *testing.T +} + +func (s *sendViaIBCTransferContract) Execute(code wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + var in startTransfer + if err := json.Unmarshal(executeMsg, &in); err != nil { + return nil, 0, err + } + ibcMsg := &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ToAddress: in.ReceiverAddr, + Amount: wasmvmtypes.NewCoin(in.CoinsToSend.Amount.Uint64(), in.CoinsToSend.Denom), + ChannelID: in.ChannelID, + Timeout: wasmvmtypes.IBCTimeout{Block: &wasmvmtypes.IBCTimeoutBlock{ + Revision: 0, + Height: 110, + }}, + }, + } + + return &wasmvmtypes.Response{Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{IBC: ibcMsg}}}}, 0, nil +} + +var _ wasmtesting.IBCContractCallbacks = &sendEmulatedIBCTransferContract{} + +// contract that interacts as an ics20 sending side via IBC packets +// It can also handle the timeout. +type sendEmulatedIBCTransferContract struct { + contractStub + t *testing.T + contractAddr string +} + +func (s *sendEmulatedIBCTransferContract) Execute(code wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + var in startTransfer + if err := json.Unmarshal(executeMsg, &in); err != nil { + return nil, 0, err + } + require.Len(s.t, info.Funds, 1) + require.Equal(s.t, in.CoinsToSend.Amount.String(), info.Funds[0].Amount) + require.Equal(s.t, in.CoinsToSend.Denom, info.Funds[0].Denom) + dataPacket := ibctransfertypes.NewFungibleTokenPacketData( + in.CoinsToSend.Denom, in.CoinsToSend.Amount.String(), info.Sender, in.ReceiverAddr, + ) + if err := dataPacket.ValidateBasic(); err != nil { + return nil, 0, err + } + + ibcMsg := &wasmvmtypes.IBCMsg{ + SendPacket: &wasmvmtypes.SendPacketMsg{ + ChannelID: in.ChannelID, + Data: dataPacket.GetBytes(), + Timeout: wasmvmtypes.IBCTimeout{Timestamp: in.Timeout}, + }, + } + return &wasmvmtypes.Response{Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{IBC: ibcMsg}}}}, 0, nil +} + +func (c *sendEmulatedIBCTransferContract) IBCPacketTimeout(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + packet := msg.Packet + + var data ibctransfertypes.FungibleTokenPacketData + if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(packet.Data, &data); err != nil { + return nil, 0, err + } + if err := data.ValidateBasic(); err != nil { + return nil, 0, err + } + amount, _ := sdk.NewIntFromString(data.Amount) + + returnTokens := &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: data.Sender, + Amount: wasmvmtypes.Coins{wasmvmtypes.NewCoin(amount.Uint64(), data.Denom)}, + }, + } + + return &wasmvmtypes.IBCBasicResponse{Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{Bank: returnTokens}}}}, 0, nil +} + +var _ wasmtesting.IBCContractCallbacks = &closeChannelContract{} + +type closeChannelContract struct { + contractStub + t *testing.T +} + +func (c *closeChannelContract) IBCChannelClose(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + return &wasmvmtypes.IBCBasicResponse{}, 1, nil +} + +func (s *closeChannelContract) Execute(code wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + var in closeIBCChannel + if err := json.Unmarshal(executeMsg, &in); err != nil { + return nil, 0, err + } + ibcMsg := &wasmvmtypes.IBCMsg{ + CloseChannel: &wasmvmtypes.CloseChannelMsg{ + ChannelID: in.ChannelID, + }, + } + + return &wasmvmtypes.Response{Messages: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyNever, Msg: wasmvmtypes.CosmosMsg{IBC: ibcMsg}}}}, 0, nil +} + +type closeIBCChannel struct { + ChannelID string +} + +func (g closeIBCChannel) GetBytes() types.RawContractMessage { + b, err := json.Marshal(g) + if err != nil { + panic(err) + } + return b +} + +// custom contract execute payload +type startTransfer struct { + ChannelID string + CoinsToSend sdk.Coin + ReceiverAddr string + ContractIBCPort string + Timeout uint64 +} + +func (g startTransfer) GetBytes() types.RawContractMessage { + b, err := json.Marshal(g) + if err != nil { + panic(err) + } + return b +} + +var _ wasmtesting.IBCContractCallbacks = &ackReceiverContract{} + +// contract that acts as the receiving side for an ics-20 transfer. +type ackReceiverContract struct { + contractStub + t *testing.T + chain *wasmibctesting.TestChain +} + +func (c *ackReceiverContract) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + packet := msg.Packet + + var src ibctransfertypes.FungibleTokenPacketData + if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(packet.Data, &src); err != nil { + return nil, 0, err + } + require.NoError(c.t, src.ValidateBasic()) + + // call original ibctransfer keeper to not copy all code into this + ibcPacket := toIBCPacket(packet) + ctx := c.chain.GetContext() // HACK: please note that this is not reverted after checkTX + err := c.chain.App.TransferKeeper.OnRecvPacket(ctx, ibcPacket, src) + if err != nil { + return nil, 0, sdkerrors.Wrap(err, "within our smart contract") + } + + var log []wasmvmtypes.EventAttribute // note: all events are under `wasm` event type + ack := channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement() + return &wasmvmtypes.IBCReceiveResult{Ok: &wasmvmtypes.IBCReceiveResponse{Acknowledgement: ack, Attributes: log}}, 0, nil +} + +func (c *ackReceiverContract) IBCPacketAck(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + var data ibctransfertypes.FungibleTokenPacketData + if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(msg.OriginalPacket.Data, &data); err != nil { + return nil, 0, err + } + // call original ibctransfer keeper to not copy all code into this + + var ack channeltypes.Acknowledgement + if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(msg.Acknowledgement.Data, &ack); err != nil { + return nil, 0, err + } + + // call original ibctransfer keeper to not copy all code into this + ctx := c.chain.GetContext() // HACK: please note that this is not reverted after checkTX + ibcPacket := toIBCPacket(msg.OriginalPacket) + err := c.chain.App.TransferKeeper.OnAcknowledgementPacket(ctx, ibcPacket, data, ack) + if err != nil { + return nil, 0, sdkerrors.Wrap(err, "within our smart contract") + } + + return &wasmvmtypes.IBCBasicResponse{}, 0, nil +} + +// contract that acts as the receiving side for an ics-20 transfer and always returns a nack. +type nackReceiverContract struct { + contractStub + t *testing.T +} + +func (c *nackReceiverContract) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + packet := msg.Packet + + var src ibctransfertypes.FungibleTokenPacketData + if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(packet.Data, &src); err != nil { + return nil, 0, err + } + require.NoError(c.t, src.ValidateBasic()) + return &wasmvmtypes.IBCReceiveResult{Err: "nack-testing"}, 0, nil +} + +// contract that acts as the receiving side for an ics-20 transfer and always returns an error. +type errorReceiverContract struct { + contractStub + t *testing.T +} + +func (c *errorReceiverContract) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + packet := msg.Packet + + var src ibctransfertypes.FungibleTokenPacketData + if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(packet.Data, &src); err != nil { + return nil, 0, err + } + require.NoError(c.t, src.ValidateBasic()) + return nil, 0, errors.New("error-testing") +} + +// simple helper struct that implements connection setup methods. +type contractStub struct{} + +func (s *contractStub) IBCChannelOpen(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelOpenMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) { + return &wasmvmtypes.IBC3ChannelOpenResponse{}, 0, nil +} + +func (s *contractStub) IBCChannelConnect(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelConnectMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + return &wasmvmtypes.IBCBasicResponse{}, 0, nil +} + +func (s *contractStub) IBCChannelClose(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCChannelCloseMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + panic("implement me") +} + +func (s *contractStub) IBCPacketReceive(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error) { + panic("implement me") +} + +func (s *contractStub) IBCPacketAck(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketAckMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + return &wasmvmtypes.IBCBasicResponse{}, 0, nil +} + +func (s *contractStub) IBCPacketTimeout(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCPacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResponse, uint64, error) { + panic("implement me") +} + +func toIBCPacket(p wasmvmtypes.IBCPacket) channeltypes.Packet { + var height clienttypes.Height + if p.Timeout.Block != nil { + height = clienttypes.NewHeight(p.Timeout.Block.Revision, p.Timeout.Block.Height) + } + return channeltypes.Packet{ + Sequence: p.Sequence, + SourcePort: p.Src.PortID, + SourceChannel: p.Src.ChannelID, + DestinationPort: p.Dest.PortID, + DestinationChannel: p.Dest.ChannelID, + Data: p.Data, + TimeoutHeight: height, + TimeoutTimestamp: p.Timeout.Timestamp, + } +} diff --git a/x/wasm/simulation/genesis.go b/x/wasm/simulation/genesis.go new file mode 100644 index 00000000..729d385e --- /dev/null +++ b/x/wasm/simulation/genesis.go @@ -0,0 +1,27 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// RandomizeGenState generates a random GenesisState for wasm +func RandomizedGenState(simstate *module.SimulationState) { + params := types.DefaultParams() + wasmGenesis := types.GenesisState{ + Params: params, + Codes: nil, + Contracts: nil, + Sequences: []types.Sequence{ + {IDKey: types.KeyLastCodeID, Value: simstate.Rand.Uint64()}, + }, + } + + _, err := simstate.Cdc.MarshalJSON(&wasmGenesis) + if err != nil { + panic(err) + } + + simstate.GenState[types.ModuleName] = simstate.Cdc.MustMarshalJSON(&wasmGenesis) +} diff --git a/x/wasm/simulation/operations.go b/x/wasm/simulation/operations.go new file mode 100644 index 00000000..c44c5a05 --- /dev/null +++ b/x/wasm/simulation/operations.go @@ -0,0 +1,551 @@ +package simulation + +import ( + "encoding/json" + "math/rand" + "os" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/baseapp" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/cerc-io/laconicd/app/params" + wasmkeeper "github.com/cerc-io/laconicd/x/wasm/keeper" + "github.com/cerc-io/laconicd/x/wasm/keeper/testdata" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +// Simulation operation weights constants +// +//nolint:gosec +const ( + OpWeightMsgStoreCode = "op_weight_msg_store_code" + OpWeightMsgInstantiateContract = "op_weight_msg_instantiate_contract" + OpWeightMsgExecuteContract = "op_weight_msg_execute_contract" + OpWeightMsgUpdateAdmin = "op_weight_msg_update_admin" + OpWeightMsgClearAdmin = "op_weight_msg_clear_admin" + OpWeightMsgMigrateContract = "op_weight_msg_migrate_contract" + OpReflectContractPath = "op_reflect_contract_path" +) + +// WasmKeeper is a subset of the wasm keeper used by simulations +type WasmKeeper interface { + GetParams(ctx sdk.Context) types.Params + IterateCodeInfos(ctx sdk.Context, cb func(uint64, types.CodeInfo) bool) + IterateContractInfo(ctx sdk.Context, cb func(sdk.AccAddress, types.ContractInfo) bool) + QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) + PeekAutoIncrementID(ctx sdk.Context, lastIDKey []byte) uint64 +} +type BankKeeper interface { + simulation.BankKeeper + IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool +} + +// WeightedOperations returns all the operations from the module with their respective weights +func WeightedOperations( + simstate *module.SimulationState, + ak types.AccountKeeper, + bk BankKeeper, + wasmKeeper WasmKeeper, +) simulation.WeightedOperations { + var ( + weightMsgStoreCode int + weightMsgInstantiateContract int + weightMsgExecuteContract int + weightMsgUpdateAdmin int + weightMsgClearAdmin int + weightMsgMigrateContract int + wasmContractPath string + ) + + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpWeightMsgStoreCode, &weightMsgStoreCode, nil, + func(_ *rand.Rand) { + weightMsgStoreCode = params.DefaultWeightMsgStoreCode + }, + ) + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpWeightMsgInstantiateContract, &weightMsgInstantiateContract, nil, + func(_ *rand.Rand) { + weightMsgInstantiateContract = params.DefaultWeightMsgInstantiateContract + }, + ) + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpWeightMsgExecuteContract, &weightMsgInstantiateContract, nil, + func(_ *rand.Rand) { + weightMsgExecuteContract = params.DefaultWeightMsgExecuteContract + }, + ) + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpWeightMsgUpdateAdmin, &weightMsgUpdateAdmin, nil, + func(_ *rand.Rand) { + weightMsgUpdateAdmin = params.DefaultWeightMsgUpdateAdmin + }, + ) + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpWeightMsgClearAdmin, &weightMsgClearAdmin, nil, + func(_ *rand.Rand) { + weightMsgClearAdmin = params.DefaultWeightMsgClearAdmin + }, + ) + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpWeightMsgMigrateContract, &weightMsgMigrateContract, nil, + func(_ *rand.Rand) { + weightMsgMigrateContract = params.DefaultWeightMsgMigrateContract + }, + ) + simstate.AppParams.GetOrGenerate(simstate.Cdc, OpReflectContractPath, &wasmContractPath, nil, + func(_ *rand.Rand) { + wasmContractPath = "" + }, + ) + + var wasmBz []byte + if wasmContractPath == "" { + wasmBz = testdata.MigrateReflectContractWasm() + } else { + var err error + wasmBz, err = os.ReadFile(wasmContractPath) + if err != nil { + panic(err) + } + } + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgStoreCode, + SimulateMsgStoreCode(ak, bk, wasmKeeper, wasmBz, 5_000_000), + ), + simulation.NewWeightedOperation( + weightMsgInstantiateContract, + SimulateMsgInstantiateContract(ak, bk, wasmKeeper, DefaultSimulationCodeIDSelector), + ), + simulation.NewWeightedOperation( + weightMsgExecuteContract, + SimulateMsgExecuteContract( + ak, + bk, + wasmKeeper, + DefaultSimulationExecuteContractSelector, + DefaultSimulationExecuteSenderSelector, + DefaultSimulationExecutePayloader, + ), + ), + simulation.NewWeightedOperation( + weightMsgUpdateAdmin, + SimulateMsgUpdateAmin( + ak, + bk, + wasmKeeper, + DefaultSimulationUpdateAdminContractSelector, + ), + ), + simulation.NewWeightedOperation( + weightMsgClearAdmin, + SimulateMsgClearAdmin( + ak, + bk, + wasmKeeper, + DefaultSimulationClearAdminContractSelector, + ), + ), + simulation.NewWeightedOperation( + weightMsgMigrateContract, + SimulateMsgMigrateContract( + ak, + bk, + wasmKeeper, + DefaultSimulationMigrateContractSelector, + DefaultSimulationMigrateCodeIDSelector, + ), + ), + } +} + +type ( + MsgMigrateContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo) + MsgMigrateCodeIDSelector func(sdk.Context, WasmKeeper, uint64) uint64 +) + +func DefaultSimulationMigrateContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) (sdk.AccAddress, types.ContractInfo) { + var contractAddress sdk.AccAddress + var contractInfo types.ContractInfo + wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool { + if info.Admin != adminAddress { + return false + } + contractAddress = address + contractInfo = info + return true + }) + return contractAddress, contractInfo +} + +func DefaultSimulationMigrateCodeIDSelector(ctx sdk.Context, wasmKeeper WasmKeeper, currentCodeID uint64) uint64 { + var codeID uint64 + wasmKeeper.IterateCodeInfos(ctx, func(u uint64, info types.CodeInfo) bool { + if (info.InstantiateConfig.Permission != types.AccessTypeEverybody) || (u == currentCodeID) { + return false + } + codeID = u + return true + }) + return codeID +} + +func SimulateMsgMigrateContract( + ak types.AccountKeeper, + bk BankKeeper, + wasmKeeper WasmKeeper, + contractSelector MsgMigrateContractSelector, + codeIDSelector MsgMigrateCodeIDSelector, +) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + ctAddress, info := contractSelector(ctx, wasmKeeper, simAccount.Address.String()) + if ctAddress == nil { + return simtypes.NoOpMsg(types.ModuleName, types.MsgMigrateContract{}.Type(), "no contract instance available"), nil, nil + } + + codeID := codeIDSelector(ctx, wasmKeeper, info.CodeID) + if codeID == 0 { + return simtypes.NoOpMsg(types.ModuleName, types.MsgMigrateContract{}.Type(), "no target contract available"), nil, nil + } + migrateMsg := types.MsgMigrateContract{ + Sender: simAccount.Address.String(), + Contract: ctAddress.String(), + CodeID: codeID, + Msg: []byte(`{}`), + } + + txCtx := BuildOperationInput(r, app, ctx, &migrateMsg, simAccount, ak, bk, nil) + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +type MsgClearAdminContractSelector func(sdk.Context, WasmKeeper, string) sdk.AccAddress + +func DefaultSimulationClearAdminContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) sdk.AccAddress { + var ctAddress sdk.AccAddress + wasmKeeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, info types.ContractInfo) bool { + if info.Admin != adminAddress { + return false + } + ctAddress = addr + return true + }) + return ctAddress +} + +func SimulateMsgClearAdmin( + ak types.AccountKeeper, + bk BankKeeper, + wasmKeeper WasmKeeper, + contractSelector MsgClearAdminContractSelector, +) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accounts []simtypes.Account, + chainID string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + simAccount, _ := simtypes.RandomAcc(r, accounts) + ctAddress := contractSelector(ctx, wasmKeeper, simAccount.Address.String()) + if ctAddress == nil { + return simtypes.NoOpMsg(types.ModuleName, types.MsgClearAdmin{}.Type(), "no contract instance available"), nil, nil + } + + msg := types.MsgClearAdmin{ + Sender: simAccount.Address.String(), + Contract: ctAddress.String(), + } + txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil) + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +type MsgUpdateAdminContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo) + +// DefaultSimulationUpdateAdminContractSelector picks the first contract which Admin != "" +func DefaultSimulationUpdateAdminContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) (sdk.AccAddress, types.ContractInfo) { + var contractAddress sdk.AccAddress + var contractInfo types.ContractInfo + wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool { + if info.Admin != adminAddress { + return false + } + contractAddress = address + contractInfo = info + return true + }) + return contractAddress, contractInfo +} + +func SimulateMsgUpdateAmin( + ak types.AccountKeeper, + bk BankKeeper, + wasmKeeper WasmKeeper, + contractSelector MsgUpdateAdminContractSelector, +) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + ctAddress, _ := contractSelector(ctx, wasmKeeper, simAccount.Address.String()) + if ctAddress == nil { + return simtypes.NoOpMsg(types.ModuleName, types.MsgUpdateAdmin{}.Type(), "no contract instance available"), nil, nil + } + + newAdmin, _ := simtypes.RandomAcc(r, accs) + if newAdmin.Address.String() == simAccount.Address.String() { + return simtypes.NoOpMsg(types.ModuleName, types.MsgUpdateAdmin{}.Type(), "new admin cannot be the same as current admin"), nil, nil + } + + msg := types.MsgUpdateAdmin{ + Sender: simAccount.Address.String(), + NewAdmin: newAdmin.Address.String(), + Contract: ctAddress.String(), + } + txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil) + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateMsgStoreCode generates a MsgStoreCode with random values +func SimulateMsgStoreCode(ak types.AccountKeeper, bk BankKeeper, wasmKeeper WasmKeeper, wasmBz []byte, gas uint64) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + if wasmKeeper.GetParams(ctx).CodeUploadAccess.Permission != types.AccessTypeEverybody { + return simtypes.NoOpMsg(types.ModuleName, types.MsgStoreCode{}.Type(), "no chain permission"), nil, nil + } + + simAccount, _ := simtypes.RandomAcc(r, accs) + + permission := wasmKeeper.GetParams(ctx).InstantiateDefaultPermission + config := permission.With(simAccount.Address) + + msg := types.MsgStoreCode{ + Sender: simAccount.Address.String(), + WASMByteCode: wasmBz, + InstantiatePermission: &config, + } + txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil) + return GenAndDeliverTxWithRandFees(txCtx, gas) + } +} + +// CodeIDSelector returns code id to be used in simulations +type CodeIDSelector = func(ctx sdk.Context, wasmKeeper WasmKeeper) uint64 + +// DefaultSimulationCodeIDSelector picks the first code id +func DefaultSimulationCodeIDSelector(ctx sdk.Context, wasmKeeper WasmKeeper) uint64 { + var codeID uint64 + wasmKeeper.IterateCodeInfos(ctx, func(u uint64, info types.CodeInfo) bool { + if info.InstantiateConfig.Permission != types.AccessTypeEverybody { + return false + } + codeID = u + return true + }) + return codeID +} + +// SimulateMsgInstantiateContract generates a MsgInstantiateContract with random values +func SimulateMsgInstantiateContract(ak types.AccountKeeper, bk BankKeeper, wasmKeeper WasmKeeper, codeSelector CodeIDSelector) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + + codeID := codeSelector(ctx, wasmKeeper) + if codeID == 0 { + return simtypes.NoOpMsg(types.ModuleName, types.MsgInstantiateContract{}.Type(), "no codes with permission available"), nil, nil + } + deposit := sdk.Coins{} + spendableCoins := bk.SpendableCoins(ctx, simAccount.Address) + for _, v := range spendableCoins { + if bk.IsSendEnabledCoin(ctx, v) { + deposit = deposit.Add(simtypes.RandSubsetCoins(r, sdk.NewCoins(v))...) + } + } + + adminAccount, _ := simtypes.RandomAcc(r, accs) + + msg := types.MsgInstantiateContract{ + Sender: simAccount.Address.String(), + Admin: adminAccount.Address.String(), + CodeID: codeID, + Label: simtypes.RandStringOfLength(r, 10), + Msg: []byte(`{}`), + Funds: deposit, + } + txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, deposit) + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// MsgExecuteContractSelector returns contract address to be used in simulations +type MsgExecuteContractSelector = func(ctx sdk.Context, wasmKeeper WasmKeeper) sdk.AccAddress + +// MsgExecutePayloader extension point to modify msg with custom payload +type MsgExecutePayloader func(msg *types.MsgExecuteContract) error + +// MsgExecuteSenderSelector extension point that returns the sender address +type MsgExecuteSenderSelector func(wasmKeeper WasmKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, accs []simtypes.Account) (simtypes.Account, error) + +// SimulateMsgExecuteContract create a execute message a reflect contract instance +func SimulateMsgExecuteContract( + ak types.AccountKeeper, + bk BankKeeper, + wasmKeeper WasmKeeper, + contractSelector MsgExecuteContractSelector, + senderSelector MsgExecuteSenderSelector, + payloader MsgExecutePayloader, +) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + contractAddr := contractSelector(ctx, wasmKeeper) + if contractAddr == nil { + return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "no contract instance available"), nil, nil + } + simAccount, err := senderSelector(wasmKeeper, ctx, contractAddr, accs) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "query contract owner"), nil, err + } + + deposit := sdk.Coins{} + spendableCoins := bk.SpendableCoins(ctx, simAccount.Address) + for _, v := range spendableCoins { + if bk.IsSendEnabledCoin(ctx, v) { + deposit = deposit.Add(simtypes.RandSubsetCoins(r, sdk.NewCoins(v))...) + } + } + if deposit.IsZero() { + return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "broke account"), nil, nil + } + msg := types.MsgExecuteContract{ + Sender: simAccount.Address.String(), + Contract: contractAddr.String(), + Funds: deposit, + } + if err := payloader(&msg); err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "contract execute payload"), nil, err + } + + txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, deposit) + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// BuildOperationInput helper to build object +func BuildOperationInput( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + msg interface { + sdk.Msg + Type() string + }, + simAccount simtypes.Account, + ak types.AccountKeeper, + bk BankKeeper, + deposit sdk.Coins, +) simulation.OperationInput { + return simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: deposit, + } +} + +// DefaultSimulationExecuteContractSelector picks the first contract address +func DefaultSimulationExecuteContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper) sdk.AccAddress { + var r sdk.AccAddress + wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool { + r = address + return true + }) + return r +} + +// DefaultSimulationExecuteSenderSelector queries reflect contract for owner address and selects accounts +func DefaultSimulationExecuteSenderSelector(wasmKeeper WasmKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, accs []simtypes.Account) (simtypes.Account, error) { + var none simtypes.Account + bz, err := json.Marshal(testdata.ReflectQueryMsg{Owner: &struct{}{}}) + if err != nil { + return none, sdkerrors.Wrap(err, "build smart query") + } + got, err := wasmKeeper.QuerySmart(ctx, contractAddr, bz) + if err != nil { + return none, sdkerrors.Wrap(err, "exec smart query") + } + var ownerRes testdata.OwnerResponse + if err := json.Unmarshal(got, &ownerRes); err != nil || ownerRes.Owner == "" { + return none, sdkerrors.Wrap(err, "parse smart query response") + } + ownerAddr, err := sdk.AccAddressFromBech32(ownerRes.Owner) + if err != nil { + return none, sdkerrors.Wrap(err, "parse contract owner address") + } + simAccount, ok := simtypes.FindAccount(accs, ownerAddr) + if !ok { + return none, sdkerrors.Wrap(err, "unknown contract owner address") + } + return simAccount, nil +} + +// DefaultSimulationExecutePayloader implements a bank msg to send the +// tokens from contract account back to original sender +func DefaultSimulationExecutePayloader(msg *types.MsgExecuteContract) error { + reflectSend := testdata.ReflectHandleMsg{ + Reflect: &testdata.ReflectPayload{ + Msgs: []wasmvmtypes.CosmosMsg{{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: msg.Sender, // + Amount: wasmkeeper.ConvertSdkCoinsToWasmCoins(msg.Funds), + }, + }, + }}, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + if err != nil { + return err + } + msg.Msg = reflectSendBz + return nil +} diff --git a/x/wasm/simulation/params.go b/x/wasm/simulation/params.go new file mode 100644 index 00000000..516fe0a9 --- /dev/null +++ b/x/wasm/simulation/params.go @@ -0,0 +1,32 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/codec" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/cerc-io/laconicd/x/wasm/types" +) + +func ParamChanges(r *rand.Rand, cdc codec.Codec) []simtypes.ParamChange { + params := types.DefaultParams() + return []simtypes.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyUploadAccess), + func(r *rand.Rand) string { + jsonBz, err := cdc.MarshalJSON(¶ms.CodeUploadAccess) + if err != nil { + panic(err) + } + return string(jsonBz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyInstantiateAccess), + func(r *rand.Rand) string { + return fmt.Sprintf("%q", params.CodeUploadAccess.Permission.String()) + }, + ), + } +} diff --git a/x/wasm/simulation/proposals.go b/x/wasm/simulation/proposals.go new file mode 100644 index 00000000..9b178b40 --- /dev/null +++ b/x/wasm/simulation/proposals.go @@ -0,0 +1,406 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/cerc-io/laconicd/app/params" + "github.com/cerc-io/laconicd/x/wasm/keeper/testdata" + "github.com/cerc-io/laconicd/x/wasm/types" +) + +const ( + WeightStoreCodeProposal = "weight_store_code_proposal" + WeightInstantiateContractProposal = "weight_instantiate_contract_proposal" + WeightUpdateAdminProposal = "weight_update_admin_proposal" + WeightExeContractProposal = "weight_execute_contract_proposal" + WeightClearAdminProposal = "weight_clear_admin_proposal" + WeightMigrateContractProposal = "weight_migrate_contract_proposal" + WeightSudoContractProposal = "weight_sudo_contract_proposal" + WeightPinCodesProposal = "weight_pin_codes_proposal" + WeightUnpinCodesProposal = "weight_unpin_codes_proposal" + WeightUpdateInstantiateConfigProposal = "weight_update_instantiate_config_proposal" + WeightStoreAndInstantiateContractProposal = "weight_store_and_instantiate_contract_proposal" +) + +func ProposalContents(bk BankKeeper, wasmKeeper WasmKeeper) []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{ + // simulation.NewWeightedProposalContent( + // WeightStoreCodeProposal, + // params.DefaultWeightStoreCodeProposal, + // SimulateStoreCodeProposal(wasmKeeper), + // ), + simulation.NewWeightedProposalContent( + WeightInstantiateContractProposal, + params.DefaultWeightInstantiateContractProposal, + SimulateInstantiateContractProposal( + bk, + wasmKeeper, + DefaultSimulationCodeIDSelector, + ), + ), + simulation.NewWeightedProposalContent( + WeightUpdateAdminProposal, + params.DefaultWeightUpdateAdminProposal, + SimulateUpdateAdminProposal( + wasmKeeper, + DefaultSimulateUpdateAdminProposalContractSelector, + ), + ), + simulation.NewWeightedProposalContent( + WeightExeContractProposal, + params.DefaultWeightExecuteContractProposal, + SimulateExecuteContractProposal( + bk, + wasmKeeper, + DefaultSimulationExecuteContractSelector, + DefaultSimulationExecuteSenderSelector, + DefaultSimulationExecutePayloader, + ), + ), + simulation.NewWeightedProposalContent( + WeightClearAdminProposal, + params.DefaultWeightClearAdminProposal, + SimulateClearAdminProposal( + wasmKeeper, + DefaultSimulateContractSelector, + ), + ), + simulation.NewWeightedProposalContent( + WeightMigrateContractProposal, + params.DefaultWeightMigrateContractProposal, + SimulateMigrateContractProposal( + wasmKeeper, + DefaultSimulateContractSelector, + DefaultSimulationCodeIDSelector, + ), + ), + // simulation.NewWeightedProposalContent( + // WeightSudoContractProposal, + // params.DefaultWeightSudoContractProposal, + // SimulateSudoContractProposal( + // wasmKeeper, + // DefaultSimulateContractSelector, + // ), + // ), + simulation.NewWeightedProposalContent( + WeightPinCodesProposal, + params.DefaultWeightPinCodesProposal, + SimulatePinContractProposal( + wasmKeeper, + DefaultSimulationCodeIDSelector, + ), + ), + simulation.NewWeightedProposalContent( + WeightUnpinCodesProposal, + params.DefaultWeightUnpinCodesProposal, + SimulateUnpinContractProposal( + wasmKeeper, + DefaultSimulationCodeIDSelector, + ), + ), + simulation.NewWeightedProposalContent( + WeightUpdateInstantiateConfigProposal, + params.DefaultWeightUpdateInstantiateConfigProposal, + SimulateUpdateInstantiateConfigProposal( + wasmKeeper, + DefaultSimulationCodeIDSelector, + ), + ), + // simulation.NewWeightedProposalContent( + // WeightStoreAndInstantiateContractProposal, + // params.DefaultWeightStoreAndInstantiateContractProposal, + // SimulateStoreAndInstantiateContractProposal( + // wasmKeeper, + // ), + // ), + } +} + +// simulate store code proposal (unused now) +// Current problem: out of gas (defaul gaswanted config of gov SimulateMsgSubmitProposal is 10_000_000) +// but this proposal may need more than it +func SimulateStoreCodeProposal(wasmKeeper WasmKeeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + + wasmBz := testdata.ReflectContractWasm() + + permission := wasmKeeper.GetParams(ctx).InstantiateDefaultPermission.With(simAccount.Address) + + return types.NewStoreCodeProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simAccount.Address.String(), + wasmBz, + &permission, + false, + "", + "", + []byte{}, + ) + } +} + +// Simulate instantiate contract proposal +func SimulateInstantiateContractProposal(bk BankKeeper, wasmKeeper WasmKeeper, codeSelector CodeIDSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + // admin + adminAccount, _ := simtypes.RandomAcc(r, accs) + // get codeID + codeID := codeSelector(ctx, wasmKeeper) + if codeID == 0 { + return nil + } + + return types.NewInstantiateContractProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simAccount.Address.String(), + adminAccount.Address.String(), + codeID, + simtypes.RandStringOfLength(r, 10), + []byte(`{}`), + sdk.Coins{}, + ) + } +} + +// Simulate execute contract proposal +func SimulateExecuteContractProposal( + bk BankKeeper, + wasmKeeper WasmKeeper, + contractSelector MsgExecuteContractSelector, + senderSelector MsgExecuteSenderSelector, + payloader MsgExecutePayloader, +) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + ctAddress := contractSelector(ctx, wasmKeeper) + if ctAddress == nil { + return nil + } + + simAccount, err := senderSelector(wasmKeeper, ctx, ctAddress, accs) + if err != nil { + return nil + } + + msg := types.MsgExecuteContract{ + Sender: simAccount.Address.String(), + Contract: ctAddress.String(), + Funds: sdk.Coins{}, + } + + if err := payloader(&msg); err != nil { + return nil + } + + return types.NewExecuteContractProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simAccount.Address.String(), + ctAddress.String(), + msg.Msg, + sdk.Coins{}, + ) + } +} + +type UpdateAdminContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo) + +func DefaultSimulateUpdateAdminProposalContractSelector( + ctx sdk.Context, + wasmKeeper WasmKeeper, + adminAddress string, +) (sdk.AccAddress, types.ContractInfo) { + var contractAddr sdk.AccAddress + var contractInfo types.ContractInfo + wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool { + if info.Admin != adminAddress { + return false + } + contractAddr = address + contractInfo = info + return true + }) + return contractAddr, contractInfo +} + +// Simulate update admin contract proposal +func SimulateUpdateAdminProposal(wasmKeeper WasmKeeper, contractSelector UpdateAdminContractSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + ctAddress, _ := contractSelector(ctx, wasmKeeper, simAccount.Address.String()) + if ctAddress == nil { + return nil + } + + return types.NewUpdateAdminProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simtypes.RandomAccounts(r, 1)[0].Address.String(), + ctAddress.String(), + ) + } +} + +type ClearAdminContractSelector func(sdk.Context, WasmKeeper) sdk.AccAddress + +func DefaultSimulateContractSelector( + ctx sdk.Context, + wasmKeeper WasmKeeper, +) sdk.AccAddress { + var contractAddr sdk.AccAddress + wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool { + contractAddr = address + return true + }) + return contractAddr +} + +// Simulate clear admin proposal +func SimulateClearAdminProposal(wasmKeeper WasmKeeper, contractSelector ClearAdminContractSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + ctAddress := contractSelector(ctx, wasmKeeper) + if ctAddress == nil { + return nil + } + + return types.NewClearAdminProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + ctAddress.String(), + ) + } +} + +type MigrateContractProposalContractSelector func(sdk.Context, WasmKeeper) sdk.AccAddress + +// Simulate migrate contract proposal +func SimulateMigrateContractProposal(wasmKeeper WasmKeeper, contractSelector MigrateContractProposalContractSelector, codeSelector CodeIDSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + ctAddress := contractSelector(ctx, wasmKeeper) + if ctAddress == nil { + return nil + } + + codeID := codeSelector(ctx, wasmKeeper) + if codeID == 0 { + return nil + } + + return types.NewMigrateContractProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + ctAddress.String(), + codeID, + []byte(`{}`), + ) + } +} + +type SudoContractProposalContractSelector func(sdk.Context, WasmKeeper) sdk.AccAddress + +// Simulate sudo contract proposal +func SimulateSudoContractProposal(wasmKeeper WasmKeeper, contractSelector SudoContractProposalContractSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + ctAddress := contractSelector(ctx, wasmKeeper) + if ctAddress == nil { + return nil + } + + return types.NewSudoContractProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + ctAddress.String(), + []byte(`{}`), + ) + } +} + +// Simulate pin contract proposal +func SimulatePinContractProposal(wasmKeeper WasmKeeper, codeSelector CodeIDSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + codeID := codeSelector(ctx, wasmKeeper) + if codeID == 0 { + return nil + } + + return types.NewPinCodesProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + []uint64{codeID}, + ) + } +} + +// Simulate unpin contract proposal +func SimulateUnpinContractProposal(wasmKeeper WasmKeeper, codeSelector CodeIDSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + codeID := codeSelector(ctx, wasmKeeper) + if codeID == 0 { + return nil + } + + return types.NewUnpinCodesProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + []uint64{codeID}, + ) + } +} + +// Simulate update instantiate config proposal +func SimulateUpdateInstantiateConfigProposal(wasmKeeper WasmKeeper, codeSelector CodeIDSelector) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + codeID := codeSelector(ctx, wasmKeeper) + if codeID == 0 { + return nil + } + + simAccount, _ := simtypes.RandomAcc(r, accs) + permission := wasmKeeper.GetParams(ctx).InstantiateDefaultPermission + config := permission.With(simAccount.Address) + + configUpdate := types.AccessConfigUpdate{ + CodeID: codeID, + InstantiatePermission: config, + } + + return types.NewUpdateInstantiateConfigProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + configUpdate, + ) + } +} + +func SimulateStoreAndInstantiateContractProposal(wasmKeeper WasmKeeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + adminAccount, _ := simtypes.RandomAcc(r, accs) + + wasmBz := testdata.ReflectContractWasm() + permission := wasmKeeper.GetParams(ctx).InstantiateDefaultPermission.With(simAccount.Address) + + return types.NewStoreAndInstantiateContractProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simAccount.Address.String(), + wasmBz, + "", + "", + []byte{}, + &permission, + false, + adminAccount.Address.String(), + simtypes.RandStringOfLength(r, 10), + []byte(`{}`), + sdk.Coins{}, + ) + } +} diff --git a/x/wasm/simulation/sim_utils.go b/x/wasm/simulation/sim_utils.go new file mode 100644 index 00000000..4f9a00b0 --- /dev/null +++ b/x/wasm/simulation/sim_utils.go @@ -0,0 +1,53 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/simapp/helpers" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// GenAndDeliverTxWithRandFees generates a transaction with a random fee and delivers it. +func GenAndDeliverTxWithRandFees(txCtx simulation.OperationInput, gas uint64) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + spendable := txCtx.Bankkeeper.SpendableCoins(txCtx.Context, account.GetAddress()) + + var fees sdk.Coins + var err error + + coins, hasNeg := spendable.SafeSub(txCtx.CoinsSpentInMsg) + if hasNeg { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "message doesn't leave room for fees"), nil, err + } + + fees, err = simtypes.RandomFees(txCtx.R, txCtx.Context, coins) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate fees"), nil, err + } + return GenAndDeliverTx(txCtx, fees, gas) +} + +// GenAndDeliverTx generates a transactions and delivers it. +func GenAndDeliverTx(txCtx simulation.OperationInput, fees sdk.Coins, gas uint64) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + tx, err := helpers.GenTx( + txCtx.TxGen, + []sdk.Msg{txCtx.Msg}, + fees, + gas, + txCtx.Context.ChainID(), + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + txCtx.SimAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate mock tx"), nil, err + } + + _, _, err = txCtx.App.Deliver(txCtx.TxGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to deliver tx"), nil, err + } + + return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil +} diff --git a/x/wasm/testdata/escrow_0.7.wasm b/x/wasm/testdata/escrow_0.7.wasm new file mode 100644 index 00000000..668aa74e Binary files /dev/null and b/x/wasm/testdata/escrow_0.7.wasm differ diff --git a/x/wasm/types/ante.go b/x/wasm/types/ante.go new file mode 100644 index 00000000..4c76efdf --- /dev/null +++ b/x/wasm/types/ante.go @@ -0,0 +1,24 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type contextKey int + +const ( + // private type creates an interface key for Context that cannot be accessed by any other package + contextKeyTXCount contextKey = iota +) + +// WithTXCounter stores a transaction counter value in the context +func WithTXCounter(ctx sdk.Context, counter uint32) sdk.Context { + return ctx.WithValue(contextKeyTXCount, counter) +} + +// TXCounter returns the tx counter value and found bool from the context. +// The result will be (0, false) for external queries or simulations where no counter available. +func TXCounter(ctx sdk.Context) (uint32, bool) { + val, ok := ctx.Value(contextKeyTXCount).(uint32) + return val, ok +} diff --git a/x/wasm/types/authz.go b/x/wasm/types/authz.go new file mode 100644 index 00000000..10dd2606 --- /dev/null +++ b/x/wasm/types/authz.go @@ -0,0 +1,534 @@ +package types + +import ( + "strings" + + "github.com/gogo/protobuf/proto" + + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authztypes "github.com/cosmos/cosmos-sdk/x/authz" +) + +const gasDeserializationCostPerByte = uint64(1) + +var ( + _ authztypes.Authorization = &ContractExecutionAuthorization{} + _ authztypes.Authorization = &ContractMigrationAuthorization{} + _ cdctypes.UnpackInterfacesMessage = &ContractExecutionAuthorization{} + _ cdctypes.UnpackInterfacesMessage = &ContractMigrationAuthorization{} +) + +// AuthzableWasmMsg is abstract wasm tx message that is supported in authz +type AuthzableWasmMsg interface { + GetFunds() sdk.Coins + GetMsg() RawContractMessage + GetContract() string + ValidateBasic() error +} + +// NewContractExecutionAuthorization constructor +func NewContractExecutionAuthorization(grants ...ContractGrant) *ContractExecutionAuthorization { + return &ContractExecutionAuthorization{ + Grants: grants, + } +} + +// MsgTypeURL implements Authorization.MsgTypeURL. +func (a ContractExecutionAuthorization) MsgTypeURL() string { + return sdk.MsgTypeURL(&MsgExecuteContract{}) +} + +// NewAuthz factory method to create an Authorization with updated grants +func (a ContractExecutionAuthorization) NewAuthz(g []ContractGrant) authztypes.Authorization { + return NewContractExecutionAuthorization(g...) +} + +// Accept implements Authorization.Accept. +func (a *ContractExecutionAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authztypes.AcceptResponse, error) { + return AcceptGrantedMessage[*MsgExecuteContract](ctx, a.Grants, msg, a) +} + +// ValidateBasic implements Authorization.ValidateBasic. +func (a ContractExecutionAuthorization) ValidateBasic() error { + return validateGrants(a.Grants) +} + +// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces +func (a ContractExecutionAuthorization) UnpackInterfaces(unpacker cdctypes.AnyUnpacker) error { + for _, g := range a.Grants { + if err := g.UnpackInterfaces(unpacker); err != nil { + return err + } + } + return nil +} + +// NewContractMigrationAuthorization constructor +func NewContractMigrationAuthorization(grants ...ContractGrant) *ContractMigrationAuthorization { + return &ContractMigrationAuthorization{ + Grants: grants, + } +} + +// MsgTypeURL implements Authorization.MsgTypeURL. +func (a ContractMigrationAuthorization) MsgTypeURL() string { + return sdk.MsgTypeURL(&MsgMigrateContract{}) +} + +// Accept implements Authorization.Accept. +func (a *ContractMigrationAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authztypes.AcceptResponse, error) { + return AcceptGrantedMessage[*MsgMigrateContract](ctx, a.Grants, msg, a) +} + +// NewAuthz factory method to create an Authorization with updated grants +func (a ContractMigrationAuthorization) NewAuthz(g []ContractGrant) authztypes.Authorization { + return NewContractMigrationAuthorization(g...) +} + +// ValidateBasic implements Authorization.ValidateBasic. +func (a ContractMigrationAuthorization) ValidateBasic() error { + return validateGrants(a.Grants) +} + +// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces +func (a ContractMigrationAuthorization) UnpackInterfaces(unpacker cdctypes.AnyUnpacker) error { + for _, g := range a.Grants { + if err := g.UnpackInterfaces(unpacker); err != nil { + return err + } + } + return nil +} + +func validateGrants(g []ContractGrant) error { + if len(g) == 0 { + return ErrEmpty.Wrap("grants") + } + for i, v := range g { + if err := v.ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "position %d", i) + } + } + // allow multiple grants for a contract: + // contractA:doThis:1,doThat:* has with different counters for different methods + return nil +} + +// ContractAuthzFactory factory to create an updated Authorization object +type ContractAuthzFactory interface { + NewAuthz([]ContractGrant) authztypes.Authorization +} + +// AcceptGrantedMessage determines whether this grant permits the provided sdk.Msg to be performed, +// and if so provides an upgraded authorization instance. +func AcceptGrantedMessage[T AuthzableWasmMsg](ctx sdk.Context, grants []ContractGrant, msg sdk.Msg, factory ContractAuthzFactory) (authztypes.AcceptResponse, error) { + exec, ok := msg.(T) + if !ok { + return authztypes.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("type mismatch") + } + if exec.GetMsg() == nil { + return authztypes.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("empty message") + } + if err := exec.ValidateBasic(); err != nil { + return authztypes.AcceptResponse{}, err + } + + // iterate though all grants + for i, g := range grants { + if g.Contract != exec.GetContract() { + continue + } + + // first check limits + result, err := g.GetLimit().Accept(ctx, exec) + switch { + case err != nil: + return authztypes.AcceptResponse{}, sdkerrors.Wrap(err, "limit") + case result == nil: // sanity check + return authztypes.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("limit result must not be nil") + case !result.Accepted: + // not applicable, continue with next grant + continue + } + + // then check permission set + ok, err := g.GetFilter().Accept(ctx, exec.GetMsg()) + switch { + case err != nil: + return authztypes.AcceptResponse{}, sdkerrors.Wrap(err, "filter") + case !ok: + // no limit update and continue with next grant + continue + } + + // finally do limit state updates in result + switch { + case result.DeleteLimit: + updatedGrants := append(grants[0:i], grants[i+1:]...) //nolint:gocritic + if len(updatedGrants) == 0 { // remove when empty + return authztypes.AcceptResponse{Accept: true, Delete: true}, nil + } + newAuthz := factory.NewAuthz(updatedGrants) + if err := newAuthz.ValidateBasic(); err != nil { // sanity check + return authztypes.AcceptResponse{}, ErrInvalid.Wrapf("new grant state: %s", err) + } + return authztypes.AcceptResponse{Accept: true, Updated: newAuthz}, nil + case result.UpdateLimit != nil: + obj, err := g.WithNewLimits(result.UpdateLimit) + if err != nil { + return authztypes.AcceptResponse{}, err + } + newAuthz := factory.NewAuthz(append(append(grants[0:i], *obj), grants[i+1:]...)) + if err := newAuthz.ValidateBasic(); err != nil { // sanity check + return authztypes.AcceptResponse{}, ErrInvalid.Wrapf("new grant state: %s", err) + } + return authztypes.AcceptResponse{Accept: true, Updated: newAuthz}, nil + default: // accepted without a limit state update + return authztypes.AcceptResponse{Accept: true}, nil + } + } + return authztypes.AcceptResponse{Accept: false}, nil +} + +// ContractAuthzLimitX define execution limits that are enforced and updated when the grant +// is applied. When the limit lapsed the grant is removed. +type ContractAuthzLimitX interface { + Accept(ctx sdk.Context, msg AuthzableWasmMsg) (*ContractAuthzLimitAcceptResult, error) + ValidateBasic() error +} + +// ContractAuthzLimitAcceptResult result of the ContractAuthzLimitX.Accept method +type ContractAuthzLimitAcceptResult struct { + // Accepted is true when limit applies + Accepted bool + // DeleteLimit when set it is the end of life for this limit. Grant is removed from persistent store + DeleteLimit bool + // UpdateLimit update persistent state with new value + UpdateLimit ContractAuthzLimitX +} + +// ContractAuthzFilterX define more fine-grained control on the message payload passed +// to the contract in the operation. When no filter applies on execution, the +// operation is prohibited. +type ContractAuthzFilterX interface { + // Accept returns applicable or error + Accept(ctx sdk.Context, msg RawContractMessage) (bool, error) + ValidateBasic() error +} + +var _ cdctypes.UnpackInterfacesMessage = &ContractGrant{} + +// NewContractGrant constructor +func NewContractGrant(contract sdk.AccAddress, limit ContractAuthzLimitX, filter ContractAuthzFilterX) (*ContractGrant, error) { + pFilter, ok := filter.(proto.Message) + if !ok { + return nil, sdkerrors.ErrInvalidType.Wrap("filter is not a proto type") + } + anyFilter, err := cdctypes.NewAnyWithValue(pFilter) + if err != nil { + return nil, sdkerrors.Wrap(err, "filter") + } + return ContractGrant{ + Contract: contract.String(), + Filter: anyFilter, + }.WithNewLimits(limit) +} + +// WithNewLimits factory method to create a new grant with given limit +func (g ContractGrant) WithNewLimits(limit ContractAuthzLimitX) (*ContractGrant, error) { + pLimit, ok := limit.(proto.Message) + if !ok { + return nil, sdkerrors.ErrInvalidType.Wrap("limit is not a proto type") + } + anyLimit, err := cdctypes.NewAnyWithValue(pLimit) + if err != nil { + return nil, sdkerrors.Wrap(err, "limit") + } + + return &ContractGrant{ + Contract: g.Contract, + Limit: anyLimit, + Filter: g.Filter, + }, nil +} + +// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces +func (g ContractGrant) UnpackInterfaces(unpacker cdctypes.AnyUnpacker) error { + var f ContractAuthzFilterX + if err := unpacker.UnpackAny(g.Filter, &f); err != nil { + return sdkerrors.Wrap(err, "filter") + } + var l ContractAuthzLimitX + if err := unpacker.UnpackAny(g.Limit, &l); err != nil { + return sdkerrors.Wrap(err, "limit") + } + return nil +} + +// GetLimit returns the cached value from the ContractGrant.Limit if present. +func (g ContractGrant) GetLimit() ContractAuthzLimitX { + if g.Limit == nil { + return &UndefinedLimit{} + } + a, ok := g.Limit.GetCachedValue().(ContractAuthzLimitX) + if !ok { + return &UndefinedLimit{} + } + return a +} + +// GetFilter returns the cached value from the ContractGrant.Filter if present. +func (g ContractGrant) GetFilter() ContractAuthzFilterX { + if g.Filter == nil { + return &UndefinedFilter{} + } + a, ok := g.Filter.GetCachedValue().(ContractAuthzFilterX) + if !ok { + return &UndefinedFilter{} + } + return a +} + +// ValidateBasic validates the grant +func (g ContractGrant) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(g.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + // execution limits + if err := g.GetLimit().ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "limit") + } + // filter + if err := g.GetFilter().ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "filter") + } + return nil +} + +// UndefinedFilter null object that is always rejected in execution +type UndefinedFilter struct{} + +// Accept always returns error +func (f *UndefinedFilter) Accept(ctx sdk.Context, msg RawContractMessage) (bool, error) { + return false, sdkerrors.ErrNotFound.Wrapf("undefined filter") +} + +// ValidateBasic always returns error +func (f UndefinedFilter) ValidateBasic() error { + return sdkerrors.ErrInvalidType.Wrapf("undefined filter") +} + +// NewAllowAllMessagesFilter constructor +func NewAllowAllMessagesFilter() *AllowAllMessagesFilter { + return &AllowAllMessagesFilter{} +} + +// Accept accepts any valid json message content. +func (f *AllowAllMessagesFilter) Accept(ctx sdk.Context, msg RawContractMessage) (bool, error) { + return true, msg.ValidateBasic() +} + +// ValidateBasic returns always nil +func (f AllowAllMessagesFilter) ValidateBasic() error { + return nil +} + +// NewAcceptedMessageKeysFilter constructor +func NewAcceptedMessageKeysFilter(acceptedKeys ...string) *AcceptedMessageKeysFilter { + return &AcceptedMessageKeysFilter{Keys: acceptedKeys} +} + +// Accept only payload messages which contain one of the accepted key names in the json object. +func (f *AcceptedMessageKeysFilter) Accept(ctx sdk.Context, msg RawContractMessage) (bool, error) { + gasForDeserialization := gasDeserializationCostPerByte * uint64(len(msg)) + ctx.GasMeter().ConsumeGas(gasForDeserialization, "contract authorization") + + ok, err := isJSONObjectWithTopLevelKey(msg, f.Keys) + if err != nil { + return false, sdkerrors.ErrUnauthorized.Wrapf("not an allowed msg: %s", err.Error()) + } + return ok, nil +} + +// ValidateBasic validates the filter +func (f AcceptedMessageKeysFilter) ValidateBasic() error { + if len(f.Keys) == 0 { + return ErrEmpty.Wrap("keys") + } + idx := make(map[string]struct{}, len(f.Keys)) + for _, m := range f.Keys { + if m == "" { + return ErrEmpty.Wrap("key") + } + if m != strings.TrimSpace(m) { + return ErrInvalid.Wrapf("key %q contains whitespaces", m) + } + if _, exists := idx[m]; exists { + return ErrDuplicate.Wrapf("key %q", m) + } + idx[m] = struct{}{} + } + return nil +} + +// NewAcceptedMessagesFilter constructor +func NewAcceptedMessagesFilter(msgs ...RawContractMessage) *AcceptedMessagesFilter { + return &AcceptedMessagesFilter{Messages: msgs} +} + +// Accept only payload messages which are equal to the granted one. +func (f *AcceptedMessagesFilter) Accept(ctx sdk.Context, msg RawContractMessage) (bool, error) { + for _, v := range f.Messages { + if v.Equal(msg) { + return true, nil + } + } + return false, nil +} + +// ValidateBasic validates the filter +func (f AcceptedMessagesFilter) ValidateBasic() error { + if len(f.Messages) == 0 { + return ErrEmpty.Wrap("messages") + } + idx := make(map[string]struct{}, len(f.Messages)) + for _, m := range f.Messages { + if len(m) == 0 { + return ErrEmpty.Wrap("message") + } + if err := m.ValidateBasic(); err != nil { + return err + } + if _, exists := idx[string(m)]; exists { + return ErrDuplicate.Wrap("message") + } + idx[string(m)] = struct{}{} + } + return nil +} + +var ( + _ ContractAuthzLimitX = &UndefinedLimit{} + _ ContractAuthzLimitX = &MaxCallsLimit{} + _ ContractAuthzLimitX = &MaxFundsLimit{} + _ ContractAuthzLimitX = &CombinedLimit{} +) + +// UndefinedLimit null object that is always rejected in execution +type UndefinedLimit struct{} + +// ValidateBasic always returns error +func (u UndefinedLimit) ValidateBasic() error { + return sdkerrors.ErrInvalidType.Wrapf("undefined limit") +} + +// Accept always returns error +func (u UndefinedLimit) Accept(ctx sdk.Context, msg AuthzableWasmMsg) (*ContractAuthzLimitAcceptResult, error) { + return nil, sdkerrors.ErrNotFound.Wrapf("undefined filter") +} + +// NewMaxCallsLimit constructor +func NewMaxCallsLimit(number uint64) *MaxCallsLimit { + return &MaxCallsLimit{Remaining: number} +} + +// Accept only the defined number of message calls. No token transfers to the contract allowed. +func (m MaxCallsLimit) Accept(_ sdk.Context, msg AuthzableWasmMsg) (*ContractAuthzLimitAcceptResult, error) { + if !msg.GetFunds().Empty() { + return &ContractAuthzLimitAcceptResult{Accepted: false}, nil + } + switch n := m.Remaining; n { + case 0: // sanity check + return nil, sdkerrors.ErrUnauthorized.Wrap("no calls left") + case 1: + return &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, nil + default: + return &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: &MaxCallsLimit{Remaining: n - 1}}, nil + } +} + +// ValidateBasic validates the limit +func (m MaxCallsLimit) ValidateBasic() error { + if m.Remaining == 0 { + return ErrEmpty.Wrap("remaining calls") + } + return nil +} + +// NewMaxFundsLimit constructor +// A panic will occur if the coin set is not valid. +func NewMaxFundsLimit(max ...sdk.Coin) *MaxFundsLimit { + return &MaxFundsLimit{Amounts: sdk.NewCoins(max...)} +} + +// Accept until the defined budget for token transfers to the contract is spent +func (m MaxFundsLimit) Accept(_ sdk.Context, msg AuthzableWasmMsg) (*ContractAuthzLimitAcceptResult, error) { + if msg.GetFunds().Empty() { // no state changes required + return &ContractAuthzLimitAcceptResult{Accepted: true}, nil + } + if !msg.GetFunds().IsAllLTE(m.Amounts) { + return &ContractAuthzLimitAcceptResult{Accepted: false}, nil + } + newAmounts := m.Amounts.Sub(msg.GetFunds()) + if newAmounts.IsZero() { + return &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, nil + } + return &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: &MaxFundsLimit{Amounts: newAmounts}}, nil +} + +// ValidateBasic validates the limit +func (m MaxFundsLimit) ValidateBasic() error { + if err := m.Amounts.Validate(); err != nil { + return err + } + if m.Amounts.IsZero() { + return ErrEmpty.Wrap("amounts") + } + return nil +} + +// NewCombinedLimit constructor +// A panic will occur if the coin set is not valid. +func NewCombinedLimit(maxCalls uint64, maxAmounts ...sdk.Coin) *CombinedLimit { + return &CombinedLimit{CallsRemaining: maxCalls, Amounts: sdk.NewCoins(maxAmounts...)} +} + +// Accept until the max calls is reached or the token budget is spent. +func (l CombinedLimit) Accept(_ sdk.Context, msg AuthzableWasmMsg) (*ContractAuthzLimitAcceptResult, error) { + transferFunds := msg.GetFunds() + if !transferFunds.IsAllLTE(l.Amounts) { + return &ContractAuthzLimitAcceptResult{Accepted: false}, nil // does not apply + } + switch n := l.CallsRemaining; n { + case 0: // sanity check + return nil, sdkerrors.ErrUnauthorized.Wrap("no calls left") + case 1: + return &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, nil + default: + remainingAmounts := l.Amounts.Sub(transferFunds) + if remainingAmounts.IsZero() { + return &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, nil + } + return &ContractAuthzLimitAcceptResult{ + Accepted: true, + UpdateLimit: NewCombinedLimit(n-1, remainingAmounts...), + }, nil + } +} + +// ValidateBasic validates the limit +func (l CombinedLimit) ValidateBasic() error { + if l.CallsRemaining == 0 { + return ErrEmpty.Wrap("remaining calls") + } + if l.Amounts.IsZero() { + return ErrEmpty.Wrap("amounts") + } + if err := l.Amounts.Validate(); err != nil { + return sdkerrors.Wrap(err, "amounts") + } + return nil +} diff --git a/x/wasm/types/authz_test.go b/x/wasm/types/authz_test.go new file mode 100644 index 00000000..06747693 --- /dev/null +++ b/x/wasm/types/authz_test.go @@ -0,0 +1,728 @@ +package types + +import ( + "math" + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authztypes "github.com/cosmos/cosmos-sdk/x/authz" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestContractAuthzFilterValidate(t *testing.T) { + specs := map[string]struct { + src ContractAuthzFilterX + expErr bool + }{ + "allow all": { + src: &AllowAllMessagesFilter{}, + }, + "allow keys - single": { + src: NewAcceptedMessageKeysFilter("foo"), + }, + "allow keys - multi": { + src: NewAcceptedMessageKeysFilter("foo", "bar"), + }, + "allow keys - empty": { + src: NewAcceptedMessageKeysFilter(), + expErr: true, + }, + "allow keys - duplicates": { + src: NewAcceptedMessageKeysFilter("foo", "foo"), + expErr: true, + }, + "allow keys - whitespaces": { + src: NewAcceptedMessageKeysFilter(" foo"), + expErr: true, + }, + "allow keys - empty key": { + src: NewAcceptedMessageKeysFilter("", "bar"), + expErr: true, + }, + "allow keys - whitespace key": { + src: NewAcceptedMessageKeysFilter(" ", "bar"), + expErr: true, + }, + "allow message - single": { + src: NewAcceptedMessagesFilter([]byte(`{}`)), + }, + "allow message - multiple": { + src: NewAcceptedMessagesFilter([]byte(`{}`), []byte(`{"foo":"bar"}`)), + }, + "allow message - multiple with empty": { + src: NewAcceptedMessagesFilter([]byte(`{}`), nil), + expErr: true, + }, + "allow message - duplicate": { + src: NewAcceptedMessagesFilter([]byte(`{}`), []byte(`{}`)), + expErr: true, + }, + "allow message - non json": { + src: NewAcceptedMessagesFilter([]byte("non-json")), + expErr: true, + }, + "allow message - empty": { + src: NewAcceptedMessagesFilter(), + expErr: true, + }, + "allow all message - always valid": { + src: NewAllowAllMessagesFilter(), + }, + "undefined - always invalid": { + src: &UndefinedFilter{}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestContractAuthzFilterAccept(t *testing.T) { + specs := map[string]struct { + filter ContractAuthzFilterX + src RawContractMessage + exp bool + expGasConsumed sdk.Gas + expErr bool + }{ + "allow all - accepts json obj": { + filter: &AllowAllMessagesFilter{}, + src: []byte(`{}`), + exp: true, + }, + "allow all - accepts json array": { + filter: &AllowAllMessagesFilter{}, + src: []byte(`[{},{}]`), + exp: true, + }, + "allow all - rejects non json msg": { + filter: &AllowAllMessagesFilter{}, + src: []byte(``), + expErr: true, + }, + "allowed key - single": { + filter: NewAcceptedMessageKeysFilter("foo"), + src: []byte(`{"foo": "bar"}`), + exp: true, + expGasConsumed: sdk.Gas(len(`{"foo": "bar"}`)), + }, + "allowed key - multiple": { + filter: NewAcceptedMessageKeysFilter("foo", "other"), + src: []byte(`{"other": "value"}`), + exp: true, + expGasConsumed: sdk.Gas(len(`{"other": "value"}`)), + }, + "allowed key - non accepted key": { + filter: NewAcceptedMessageKeysFilter("foo"), + src: []byte(`{"bar": "value"}`), + exp: false, + expGasConsumed: sdk.Gas(len(`{"bar": "value"}`)), + }, + "allowed key - unsupported array msg": { + filter: NewAcceptedMessageKeysFilter("foo", "other"), + src: []byte(`[{"foo":"bar"}]`), + expErr: false, + expGasConsumed: sdk.Gas(len(`[{"foo":"bar"}]`)), + }, + "allowed key - invalid msg": { + filter: NewAcceptedMessageKeysFilter("foo", "other"), + src: []byte(`not a json msg`), + expErr: true, + }, + "allow message - single": { + filter: NewAcceptedMessagesFilter([]byte(`{}`)), + src: []byte(`{}`), + exp: true, + }, + "allow message - multiple": { + filter: NewAcceptedMessagesFilter([]byte(`[{"foo":"bar"}]`), []byte(`{"other":"value"}`)), + src: []byte(`[{"foo":"bar"}]`), + exp: true, + }, + "allow message - no match": { + filter: NewAcceptedMessagesFilter([]byte(`{"foo":"bar"}`)), + src: []byte(`{"other":"value"}`), + exp: false, + }, + "allow all message - always accept valid": { + filter: NewAllowAllMessagesFilter(), + src: []byte(`{"other":"value"}`), + exp: true, + }, + "allow all message - always reject invalid json": { + filter: NewAllowAllMessagesFilter(), + src: []byte(`not json`), + expErr: true, + }, + "undefined - always errors": { + filter: &UndefinedFilter{}, + src: []byte(`{"foo":"bar"}`), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gm := sdk.NewGasMeter(1_000_000) + allowed, gotErr := spec.filter.Accept(sdk.Context{}.WithGasMeter(gm), spec.src) + + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, allowed) + assert.Equal(t, spec.expGasConsumed, gm.GasConsumed()) + }) + } +} + +func TestContractAuthzLimitValidate(t *testing.T) { + oneToken := sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt()) + specs := map[string]struct { + src ContractAuthzLimitX + expErr bool + }{ + "max calls": { + src: NewMaxCallsLimit(1), + }, + "max calls - max uint64": { + src: NewMaxCallsLimit(math.MaxUint64), + }, + "max calls - empty": { + src: NewMaxCallsLimit(0), + expErr: true, + }, + "max funds": { + src: NewMaxFundsLimit(oneToken), + }, + "max funds - empty coins": { + src: NewMaxFundsLimit(), + expErr: true, + }, + "max funds - duplicates": { + src: &MaxFundsLimit{Amounts: sdk.Coins{oneToken, oneToken}}, + expErr: true, + }, + "max funds - contains empty value": { + src: &MaxFundsLimit{Amounts: sdk.Coins{oneToken, sdk.NewCoin("other", sdk.ZeroInt())}.Sort()}, + expErr: true, + }, + "max funds - unsorted": { + src: &MaxFundsLimit{Amounts: sdk.Coins{oneToken, sdk.NewCoin("other", sdk.OneInt())}}, + expErr: true, + }, + "combined": { + src: NewCombinedLimit(1, oneToken), + }, + "combined - empty calls": { + src: NewCombinedLimit(0, oneToken), + expErr: true, + }, + "combined - empty amounts": { + src: NewCombinedLimit(1), + expErr: true, + }, + "combined - invalid amounts": { + src: &CombinedLimit{CallsRemaining: 1, Amounts: sdk.Coins{oneToken, oneToken}}, + expErr: true, + }, + "undefined": { + src: &UndefinedLimit{}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestContractAuthzLimitAccept(t *testing.T) { + oneToken := sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt()) + otherToken := sdk.NewCoin("other", sdk.OneInt()) + specs := map[string]struct { + limit ContractAuthzLimitX + src AuthzableWasmMsg + exp *ContractAuthzLimitAcceptResult + expErr bool + }{ + "max calls - updated": { + limit: NewMaxCallsLimit(2), + src: &MsgExecuteContract{}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: NewMaxCallsLimit(1)}, + }, + "max calls - removed": { + limit: NewMaxCallsLimit(1), + src: &MsgExecuteContract{}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, + }, + "max calls - accepted with zero fund set": { + limit: NewMaxCallsLimit(1), + src: &MsgExecuteContract{Funds: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.ZeroInt()))}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, + }, + "max calls - rejected with some fund transfer": { + limit: NewMaxCallsLimit(1), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "max calls - invalid": { + limit: &MaxCallsLimit{}, + src: &MsgExecuteContract{}, + expErr: true, + }, + "max funds - single updated": { + limit: NewMaxFundsLimit(oneToken.Add(oneToken)), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: NewMaxFundsLimit(oneToken)}, + }, + "max funds - single removed": { + limit: NewMaxFundsLimit(oneToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, + }, + "max funds - single with unknown token": { + limit: NewMaxFundsLimit(oneToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(otherToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "max funds - single exceeds limit": { + limit: NewMaxFundsLimit(oneToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken.Add(oneToken))}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "max funds - single with additional token send": { + limit: NewMaxFundsLimit(oneToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken, otherToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "max funds - multi with other left": { + limit: NewMaxFundsLimit(oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: NewMaxFundsLimit(otherToken)}, + }, + "max funds - multi with all used": { + limit: NewMaxFundsLimit(oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken, otherToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, + }, + "max funds - multi with no tokens sent": { + limit: NewMaxFundsLimit(oneToken, otherToken), + src: &MsgExecuteContract{}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true}, + }, + "max funds - multi with other exceeds limit": { + limit: NewMaxFundsLimit(oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken, otherToken.Add(otherToken))}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "max combined - multi amounts one consumed": { + limit: NewCombinedLimit(2, oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: NewCombinedLimit(1, otherToken)}, + }, + "max combined - multi amounts none consumed": { + limit: NewCombinedLimit(2, oneToken, otherToken), + src: &MsgExecuteContract{}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: NewCombinedLimit(1, oneToken, otherToken)}, + }, + "max combined - removed on last execution": { + limit: NewCombinedLimit(1, oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, + }, + "max combined - removed on last token": { + limit: NewCombinedLimit(2, oneToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, DeleteLimit: true}, + }, + "max combined - update with token and calls remaining": { + limit: NewCombinedLimit(2, oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: true, UpdateLimit: NewCombinedLimit(1, otherToken)}, + }, + "max combined - multi with other exceeds limit": { + limit: NewCombinedLimit(2, oneToken, otherToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(oneToken, otherToken.Add(otherToken))}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "max combined - with unknown token": { + limit: NewCombinedLimit(2, oneToken), + src: &MsgExecuteContract{Funds: sdk.NewCoins(otherToken)}, + exp: &ContractAuthzLimitAcceptResult{Accepted: false}, + }, + "undefined": { + limit: &UndefinedLimit{}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotResult, gotErr := spec.limit.Accept(sdk.Context{}, spec.src) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, gotResult) + }) + } +} + +func TestValidateContractGrant(t *testing.T) { + specs := map[string]struct { + setup func(t *testing.T) ContractGrant + expErr bool + }{ + "all good": { + setup: func(t *testing.T) ContractGrant { + return mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(1), NewAllowAllMessagesFilter()) + }, + }, + "invalid address": { + setup: func(t *testing.T) ContractGrant { + return mustGrant([]byte{}, NewMaxCallsLimit(1), NewAllowAllMessagesFilter()) + }, + expErr: true, + }, + "invalid limit": { + setup: func(t *testing.T) ContractGrant { + return mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(0), NewAllowAllMessagesFilter()) + }, + expErr: true, + }, + + "invalid filter ": { + setup: func(t *testing.T) ContractGrant { + return mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(1), NewAcceptedMessageKeysFilter()) + }, + expErr: true, + }, + "empty limit": { + setup: func(t *testing.T) ContractGrant { + r := mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(0), NewAllowAllMessagesFilter()) + r.Limit = nil + return r + }, + expErr: true, + }, + + "empty filter ": { + setup: func(t *testing.T) ContractGrant { + r := mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(1), NewAcceptedMessageKeysFilter()) + r.Filter = nil + return r + }, + expErr: true, + }, + "wrong limit type": { + setup: func(t *testing.T) ContractGrant { + r := mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(0), NewAllowAllMessagesFilter()) + r.Limit = r.Filter + return r + }, + expErr: true, + }, + + "wrong filter type": { + setup: func(t *testing.T) ContractGrant { + r := mustGrant(randBytes(ContractAddrLen), NewMaxCallsLimit(1), NewAcceptedMessageKeysFilter()) + r.Filter = r.Limit + return r + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := spec.setup(t).ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestValidateContractAuthorization(t *testing.T) { + validGrant, err := NewContractGrant(randBytes(SDKAddrLen), NewMaxCallsLimit(1), NewAllowAllMessagesFilter()) + require.NoError(t, err) + invalidGrant, err := NewContractGrant(randBytes(SDKAddrLen), NewMaxCallsLimit(1), NewAllowAllMessagesFilter()) + require.NoError(t, err) + invalidGrant.Limit = nil + + specs := map[string]struct { + setup func(t *testing.T) validatable + expErr bool + }{ + "contract execution": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization(*validGrant) + }, + }, + "contract execution - duplicate grants": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization(*validGrant, *validGrant) + }, + }, + "contract execution - invalid grant": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization(*validGrant, *invalidGrant) + }, + expErr: true, + }, + "contract execution - empty grants": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization() + }, + expErr: true, + }, + "contract migration": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization(*validGrant) + }, + }, + "contract migration - duplicate grants": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization(*validGrant, *validGrant) + }, + }, + "contract migration - invalid grant": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization(*validGrant, *invalidGrant) + }, + expErr: true, + }, + "contract migration - empty grant": { + setup: func(t *testing.T) validatable { + return NewContractMigrationAuthorization() + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := spec.setup(t).ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestAcceptGrantedMessage(t *testing.T) { + myContractAddr := sdk.AccAddress(randBytes(SDKAddrLen)) + otherContractAddr := sdk.AccAddress(randBytes(SDKAddrLen)) + specs := map[string]struct { + auth authztypes.Authorization + msg sdk.Msg + expResult authztypes.AcceptResponse + expErr *sdkerrors.Error + }{ + "accepted and updated - contract execution": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(2), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{ + Accept: true, + Updated: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + }, + }, + "accepted and not updated - limit not touched": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxFundsLimit(sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt())), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{Accept: true}, + }, + "accepted and removed - single": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{Accept: true, Delete: true}, + }, + "accepted and updated - multi, one removed": { + auth: NewContractExecutionAuthorization( + mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter()), + mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter()), + ), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{ + Accept: true, + Updated: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + }, + }, + "accepted and updated - multi, one updated": { + auth: NewContractExecutionAuthorization( + mustGrant(otherContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter()), + mustGrant(myContractAddr, NewMaxFundsLimit(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(2))), NewAcceptedMessageKeysFilter("bar")), + mustGrant(myContractAddr, NewCombinedLimit(2, sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(2))), NewAcceptedMessageKeysFilter("foo")), + ), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + Funds: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt())), + }, + expResult: authztypes.AcceptResponse{ + Accept: true, + Updated: NewContractExecutionAuthorization( + mustGrant(otherContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter()), + mustGrant(myContractAddr, NewMaxFundsLimit(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(2))), NewAcceptedMessageKeysFilter("bar")), + mustGrant(myContractAddr, NewCombinedLimit(1, sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1))), NewAcceptedMessageKeysFilter("foo")), + ), + }, + }, + "not accepted - no matching contract address": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{Accept: false}, + }, + "not accepted - max calls but tokens": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + Funds: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt())), + }, + expResult: authztypes.AcceptResponse{Accept: false}, + }, + "not accepted - funds exceeds limit": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxFundsLimit(sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt())), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + Funds: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(2))), + }, + expResult: authztypes.AcceptResponse{Accept: false}, + }, + "not accepted - no matching filter": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAcceptedMessageKeysFilter("other"))), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + Funds: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt())), + }, + expResult: authztypes.AcceptResponse{Accept: false}, + }, + "invalid msg type - contract execution": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgMigrateContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + CodeID: 1, + Msg: []byte(`{"foo":"bar"}`), + }, + expErr: sdkerrors.ErrInvalidType, + }, + "payload is empty": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + }, + expErr: sdkerrors.ErrInvalidType, + }, + "payload is invalid": { + auth: NewContractExecutionAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`not json`), + }, + expErr: ErrInvalid, + }, + "invalid grant": { + auth: NewContractExecutionAuthorization(ContractGrant{Contract: myContractAddr.String()}), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expErr: sdkerrors.ErrNotFound, + }, + "invalid msg type - contract migration": { + auth: NewContractMigrationAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + msg: &MsgExecuteContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + Msg: []byte(`{"foo":"bar"}`), + }, + expErr: sdkerrors.ErrInvalidType, + }, + "accepted and updated - contract migration": { + auth: NewContractMigrationAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(2), NewAllowAllMessagesFilter())), + msg: &MsgMigrateContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: myContractAddr.String(), + CodeID: 1, + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{ + Accept: true, + Updated: NewContractMigrationAuthorization(mustGrant(myContractAddr, NewMaxCallsLimit(1), NewAllowAllMessagesFilter())), + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{}.WithGasMeter(sdk.NewInfiniteGasMeter()) + gotResult, gotErr := spec.auth.Accept(ctx, spec.msg) + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expResult, gotResult) + }) + } +} + +func mustGrant(contract sdk.AccAddress, limit ContractAuthzLimitX, filter ContractAuthzFilterX) ContractGrant { + g, err := NewContractGrant(contract, limit, filter) + if err != nil { + panic(err) + } + return *g +} diff --git a/x/wasm/types/codec.go b/x/wasm/types/codec.go new file mode 100644 index 00000000..7d8ebcbe --- /dev/null +++ b/x/wasm/types/codec.go @@ -0,0 +1,122 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" + "github.com/cosmos/cosmos-sdk/x/authz" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +// RegisterLegacyAminoCodec registers the account types and interface +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { //nolint:staticcheck + cdc.RegisterConcrete(&MsgStoreCode{}, "wasm/MsgStoreCode", nil) + cdc.RegisterConcrete(&MsgInstantiateContract{}, "wasm/MsgInstantiateContract", nil) + cdc.RegisterConcrete(&MsgInstantiateContract2{}, "wasm/MsgInstantiateContract2", nil) + cdc.RegisterConcrete(&MsgExecuteContract{}, "wasm/MsgExecuteContract", nil) + cdc.RegisterConcrete(&MsgMigrateContract{}, "wasm/MsgMigrateContract", nil) + cdc.RegisterConcrete(&MsgUpdateAdmin{}, "wasm/MsgUpdateAdmin", nil) + cdc.RegisterConcrete(&MsgClearAdmin{}, "wasm/MsgClearAdmin", nil) + cdc.RegisterConcrete(&MsgUpdateInstantiateConfig{}, "wasm/MsgUpdateInstantiateConfig", nil) + + cdc.RegisterConcrete(&PinCodesProposal{}, "wasm/PinCodesProposal", nil) + cdc.RegisterConcrete(&UnpinCodesProposal{}, "wasm/UnpinCodesProposal", nil) + cdc.RegisterConcrete(&StoreCodeProposal{}, "wasm/StoreCodeProposal", nil) + cdc.RegisterConcrete(&InstantiateContractProposal{}, "wasm/InstantiateContractProposal", nil) + cdc.RegisterConcrete(&InstantiateContract2Proposal{}, "wasm/InstantiateContract2Proposal", nil) + cdc.RegisterConcrete(&MigrateContractProposal{}, "wasm/MigrateContractProposal", nil) + cdc.RegisterConcrete(&SudoContractProposal{}, "wasm/SudoContractProposal", nil) + cdc.RegisterConcrete(&ExecuteContractProposal{}, "wasm/ExecuteContractProposal", nil) + cdc.RegisterConcrete(&UpdateAdminProposal{}, "wasm/UpdateAdminProposal", nil) + cdc.RegisterConcrete(&ClearAdminProposal{}, "wasm/ClearAdminProposal", nil) + cdc.RegisterConcrete(&UpdateInstantiateConfigProposal{}, "wasm/UpdateInstantiateConfigProposal", nil) + + cdc.RegisterInterface((*ContractInfoExtension)(nil), nil) + + cdc.RegisterInterface((*ContractAuthzFilterX)(nil), nil) + cdc.RegisterConcrete(&AllowAllMessagesFilter{}, "wasm/AllowAllMessagesFilter", nil) + cdc.RegisterConcrete(&AcceptedMessageKeysFilter{}, "wasm/AcceptedMessageKeysFilter", nil) + cdc.RegisterConcrete(&AcceptedMessagesFilter{}, "wasm/AcceptedMessagesFilter", nil) + + cdc.RegisterInterface((*ContractAuthzLimitX)(nil), nil) + cdc.RegisterConcrete(&MaxCallsLimit{}, "wasm/MaxCallsLimit", nil) + cdc.RegisterConcrete(&MaxFundsLimit{}, "wasm/MaxFundsLimit", nil) + cdc.RegisterConcrete(&CombinedLimit{}, "wasm/CombinedLimit", nil) + + cdc.RegisterConcrete(&ContractExecutionAuthorization{}, "wasm/ContractExecutionAuthorization", nil) + cdc.RegisterConcrete(&ContractMigrationAuthorization{}, "wasm/ContractMigrationAuthorization", nil) + cdc.RegisterConcrete(&StoreAndInstantiateContractProposal{}, "wasm/StoreAndInstantiateContractProposal", nil) +} + +func RegisterInterfaces(registry types.InterfaceRegistry) { + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgStoreCode{}, + &MsgInstantiateContract{}, + &MsgInstantiateContract2{}, + &MsgExecuteContract{}, + &MsgMigrateContract{}, + &MsgUpdateAdmin{}, + &MsgClearAdmin{}, + &MsgIBCCloseChannel{}, + &MsgIBCSend{}, + &MsgUpdateInstantiateConfig{}, + ) + registry.RegisterImplementations( + (*govtypes.Content)(nil), + &StoreCodeProposal{}, + &InstantiateContractProposal{}, + &InstantiateContract2Proposal{}, + &MigrateContractProposal{}, + &SudoContractProposal{}, + &ExecuteContractProposal{}, + &UpdateAdminProposal{}, + &ClearAdminProposal{}, + &PinCodesProposal{}, + &UnpinCodesProposal{}, + &UpdateInstantiateConfigProposal{}, + &StoreAndInstantiateContractProposal{}, + ) + + registry.RegisterInterface("ContractInfoExtension", (*ContractInfoExtension)(nil)) + + registry.RegisterInterface("ContractAuthzFilterX", (*ContractAuthzFilterX)(nil)) + registry.RegisterImplementations( + (*ContractAuthzFilterX)(nil), + &AllowAllMessagesFilter{}, + &AcceptedMessageKeysFilter{}, + &AcceptedMessagesFilter{}, + ) + + registry.RegisterInterface("ContractAuthzLimitX", (*ContractAuthzLimitX)(nil)) + registry.RegisterImplementations( + (*ContractAuthzLimitX)(nil), + &MaxCallsLimit{}, + &MaxFundsLimit{}, + &CombinedLimit{}, + ) + + registry.RegisterImplementations( + (*authz.Authorization)(nil), + &ContractExecutionAuthorization{}, + &ContractMigrationAuthorization{}, + ) + + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} + +var ( + amino = codec.NewLegacyAmino() + + // ModuleCdc references the global x/wasm module codec. + + ModuleCdc = codec.NewAminoCodec(amino) +) + +func init() { + RegisterLegacyAminoCodec(amino) + cryptocodec.RegisterCrypto(amino) + amino.Seal() +} diff --git a/x/wasm/types/errors.go b/x/wasm/types/errors.go new file mode 100644 index 00000000..297f34d5 --- /dev/null +++ b/x/wasm/types/errors.go @@ -0,0 +1,150 @@ +package types + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdkErrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// Codes for wasm contract errors +var ( + DefaultCodespace = ModuleName + + // Note: never use code 1 for any errors - that is reserved for ErrInternal in the core cosmos sdk + + // ErrCreateFailed error for wasm code that has already been uploaded or failed + ErrCreateFailed = sdkErrors.Register(DefaultCodespace, 2, "create wasm contract failed") + + // ErrAccountExists error for a contract account that already exists + ErrAccountExists = sdkErrors.Register(DefaultCodespace, 3, "contract account already exists") + + // ErrInstantiateFailed error for rust instantiate contract failure + ErrInstantiateFailed = sdkErrors.Register(DefaultCodespace, 4, "instantiate wasm contract failed") + + // ErrExecuteFailed error for rust execution contract failure + ErrExecuteFailed = sdkErrors.Register(DefaultCodespace, 5, "execute wasm contract failed") + + // ErrGasLimit error for out of gas + ErrGasLimit = sdkErrors.Register(DefaultCodespace, 6, "insufficient gas") + + // ErrInvalidGenesis error for invalid genesis file syntax + ErrInvalidGenesis = sdkErrors.Register(DefaultCodespace, 7, "invalid genesis") + + // ErrNotFound error for an entry not found in the store + ErrNotFound = sdkErrors.Register(DefaultCodespace, 8, "not found") + + // ErrQueryFailed error for rust smart query contract failure + ErrQueryFailed = sdkErrors.Register(DefaultCodespace, 9, "query wasm contract failed") + + // ErrInvalidMsg error when we cannot process the error returned from the contract + ErrInvalidMsg = sdkErrors.Register(DefaultCodespace, 10, "invalid CosmosMsg from the contract") + + // ErrMigrationFailed error for rust execution contract failure + ErrMigrationFailed = sdkErrors.Register(DefaultCodespace, 11, "migrate wasm contract failed") + + // ErrEmpty error for empty content + ErrEmpty = sdkErrors.Register(DefaultCodespace, 12, "empty") + + // ErrLimit error for content that exceeds a limit + ErrLimit = sdkErrors.Register(DefaultCodespace, 13, "exceeds limit") + + // ErrInvalid error for content that is invalid in this context + ErrInvalid = sdkErrors.Register(DefaultCodespace, 14, "invalid") + + // ErrDuplicate error for content that exists + ErrDuplicate = sdkErrors.Register(DefaultCodespace, 15, "duplicate") + + // ErrMaxIBCChannels error for maximum number of ibc channels reached + ErrMaxIBCChannels = sdkErrors.Register(DefaultCodespace, 16, "max transfer channels") + + // ErrUnsupportedForContract error when a capability is used that is not supported for/ by this contract + ErrUnsupportedForContract = sdkErrors.Register(DefaultCodespace, 17, "unsupported for this contract") + + // ErrPinContractFailed error for pinning contract failures + ErrPinContractFailed = sdkErrors.Register(DefaultCodespace, 18, "pinning contract failed") + + // ErrUnpinContractFailed error for unpinning contract failures + ErrUnpinContractFailed = sdkErrors.Register(DefaultCodespace, 19, "unpinning contract failed") + + // ErrUnknownMsg error by a message handler to show that it is not responsible for this message type + ErrUnknownMsg = sdkErrors.Register(DefaultCodespace, 20, "unknown message from the contract") + + // ErrInvalidEvent error if an attribute/event from the contract is invalid + ErrInvalidEvent = sdkErrors.Register(DefaultCodespace, 21, "invalid event") + + // ErrNoSuchContractFn error factory for an error when an address does not belong to a contract + ErrNoSuchContractFn = WasmVMFlavouredErrorFactory(sdkErrors.Register(DefaultCodespace, 22, "no such contract"), + func(addr string) error { return wasmvmtypes.NoSuchContract{Addr: addr} }, + ) + + // code 23 -26 were used for json parser + + // ErrExceedMaxQueryStackSize error if max query stack size is exceeded + ErrExceedMaxQueryStackSize = sdkErrors.Register(DefaultCodespace, 27, "max query stack size exceeded") + + // ErrNoSuchCodeFn factory for an error when a code id does not belong to a code info + ErrNoSuchCodeFn = WasmVMFlavouredErrorFactory(sdkErrors.Register(DefaultCodespace, 28, "no such code"), + func(id uint64) error { return wasmvmtypes.NoSuchCode{CodeID: id} }, + ) +) + +// WasmVMErrorable mapped error type in wasmvm and are not redacted +type WasmVMErrorable interface { + // ToWasmVMError convert instance to wasmvm friendly error if possible otherwise root cause. never nil + ToWasmVMError() error +} + +var _ WasmVMErrorable = WasmVMFlavouredError{} + +// WasmVMFlavouredError wrapper for sdk error that supports wasmvm error types +type WasmVMFlavouredError struct { + sdkErr *sdkErrors.Error + wasmVMErr error +} + +// NewWasmVMFlavouredError constructor +func NewWasmVMFlavouredError(sdkErr *sdkErrors.Error, wasmVMErr error) WasmVMFlavouredError { + return WasmVMFlavouredError{sdkErr: sdkErr, wasmVMErr: wasmVMErr} +} + +// WasmVMFlavouredErrorFactory is a factory method to build a WasmVMFlavouredError type +func WasmVMFlavouredErrorFactory[T any](sdkErr *sdkErrors.Error, wasmVMErrBuilder func(T) error) func(T) WasmVMFlavouredError { + if wasmVMErrBuilder == nil { + panic("builder function required") + } + return func(d T) WasmVMFlavouredError { + return WasmVMFlavouredError{sdkErr: sdkErr, wasmVMErr: wasmVMErrBuilder(d)} + } +} + +// ToWasmVMError implements WasmVMError-able +func (e WasmVMFlavouredError) ToWasmVMError() error { + if e.wasmVMErr != nil { + return e.wasmVMErr + } + return e.sdkErr +} + +// implements stdlib error +func (e WasmVMFlavouredError) Error() string { + return e.sdkErr.Error() +} + +// Unwrap implements the built-in errors.Unwrap +func (e WasmVMFlavouredError) Unwrap() error { + return e.sdkErr +} + +// Cause is the same as unwrap but used by errors.abci +func (e WasmVMFlavouredError) Cause() error { + return e.Unwrap() +} + +// Wrap extends this error with additional information. +// It's a handy function to call Wrap with sdk errors. +func (e WasmVMFlavouredError) Wrap(desc string) error { return sdkErrors.Wrap(e, desc) } + +// Wrapf extends this error with additional information. +// It's a handy function to call Wrapf with sdk errors. +func (e WasmVMFlavouredError) Wrapf(desc string, args ...interface{}) error { + return sdkErrors.Wrapf(e, desc, args...) +} diff --git a/x/wasm/types/errors_test.go b/x/wasm/types/errors_test.go new file mode 100644 index 00000000..76a1c023 --- /dev/null +++ b/x/wasm/types/errors_test.go @@ -0,0 +1,86 @@ +package types + +import ( + "errors" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWasmVMFlavouredError(t *testing.T) { + myErr := ErrNoSuchCodeFn(1) + specs := map[string]struct { + exec func(t *testing.T) + }{ + "IsOf": { + exec: func(t *testing.T) { + assert.True(t, sdkerrors.IsOf(myErr, myErr.sdkErr)) + assert.Equal(t, myErr.sdkErr, myErr.Unwrap()) + }, + }, + "unwrapped": { + exec: func(t *testing.T) { + assert.Equal(t, myErr.sdkErr, myErr.Unwrap()) + }, + }, + "caused": { + exec: func(t *testing.T) { + assert.Equal(t, myErr.sdkErr, myErr.Cause()) + }, + }, + "wrapped supports WasmVMErrorable": { + exec: func(t *testing.T) { + var wasmvmErr WasmVMErrorable + require.True(t, errors.As(myErr.Wrap("my description"), &wasmvmErr)) + gotErr := wasmvmErr.ToWasmVMError() + assert.Equal(t, wasmvmtypes.NoSuchCode{CodeID: 1}, gotErr) + }, + }, + "wrappedf supports WasmVMErrorable": { + exec: func(t *testing.T) { + var wasmvmErr WasmVMErrorable + require.True(t, errors.As(myErr.Wrapf("my description: %d", 1), &wasmvmErr)) + gotErr := wasmvmErr.ToWasmVMError() + assert.Equal(t, wasmvmtypes.NoSuchCode{CodeID: 1}, gotErr) + }, + }, + "supports WasmVMErrorable": { + exec: func(t *testing.T) { + var wasmvmErr WasmVMErrorable + require.True(t, errors.As(myErr, &wasmvmErr)) + gotErr := wasmvmErr.ToWasmVMError() + assert.Equal(t, wasmvmtypes.NoSuchCode{CodeID: 1}, gotErr) + }, + }, + "fallback to sdk error when wasmvm error unset": { + exec: func(t *testing.T) { + var wasmvmErr WasmVMErrorable + require.True(t, errors.As(WasmVMFlavouredError{sdkErr: ErrEmpty}, &wasmvmErr)) + gotErr := wasmvmErr.ToWasmVMError() + assert.Equal(t, ErrEmpty, gotErr) + }, + }, + "abci info": { + exec: func(t *testing.T) { + codespace, code, log := sdkerrors.ABCIInfo(myErr, false) + assert.Equal(t, DefaultCodespace, codespace) + assert.Equal(t, uint32(28), code) + assert.Equal(t, "no such code", log) + }, + }, + "abci info - wrapped": { + exec: func(t *testing.T) { + codespace, code, log := sdkerrors.ABCIInfo(myErr.Wrap("my description"), false) + assert.Equal(t, DefaultCodespace, codespace) + assert.Equal(t, uint32(28), code) + assert.Equal(t, "my description: no such code", log) + }, + }, + } + for name, spec := range specs { + t.Run(name, spec.exec) + } +} diff --git a/x/wasm/types/events.go b/x/wasm/types/events.go new file mode 100644 index 00000000..442c3ed3 --- /dev/null +++ b/x/wasm/types/events.go @@ -0,0 +1,34 @@ +package types + +const ( + // WasmModuleEventType is stored with any contract TX that returns non empty EventAttributes + WasmModuleEventType = "wasm" + // CustomContractEventPrefix contracts can create custom events. To not mix them with other system events they got the `wasm-` prefix. + CustomContractEventPrefix = "wasm-" + + EventTypeStoreCode = "store_code" + EventTypeInstantiate = "instantiate" + EventTypeExecute = "execute" + EventTypeMigrate = "migrate" + EventTypePinCode = "pin_code" + EventTypeUnpinCode = "unpin_code" + EventTypeSudo = "sudo" + EventTypeReply = "reply" + EventTypeGovContractResult = "gov_contract_result" + EventTypeUpdateContractAdmin = "update_contract_admin" + EventTypeUpdateCodeAccessConfig = "update_code_access_config" +) + +// event attributes returned from contract execution +const ( + AttributeReservedPrefix = "_" + + AttributeKeyContractAddr = "_contract_address" + AttributeKeyCodeID = "code_id" + AttributeKeyChecksum = "code_checksum" + AttributeKeyResultDataHex = "result" + AttributeKeyRequiredCapability = "required_capability" + AttributeKeyNewAdmin = "new_admin_address" + AttributeKeyCodePermission = "code_permission" + AttributeKeyAuthorizedAddresses = "authorized_addresses" +) diff --git a/x/wasm/types/expected_keepers.go b/x/wasm/types/expected_keepers.go new file mode 100644 index 00000000..57cc4073 --- /dev/null +++ b/x/wasm/types/expected_keepers.go @@ -0,0 +1,106 @@ +package types + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + connectiontypes "github.com/cosmos/ibc-go/v4/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" +) + +// BankViewKeeper defines a subset of methods implemented by the cosmos-sdk bank keeper +type BankViewKeeper interface { + GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + GetSupply(ctx sdk.Context, denom string) sdk.Coin +} + +// Burner is a subset of the sdk bank keeper methods +type Burner interface { + BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error +} + +// BankKeeper defines a subset of methods implemented by the cosmos-sdk bank keeper +type BankKeeper interface { + BankViewKeeper + Burner + IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error + BlockedAddr(addr sdk.AccAddress) bool + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error +} + +// AccountKeeper defines a subset of methods implemented by the cosmos-sdk account keeper +type AccountKeeper interface { + // Return a new account with the next account number and the specified address. Does not save the new account to the store. + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + // Retrieve an account from the store. + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + // Set an account in the store. + SetAccount(ctx sdk.Context, acc authtypes.AccountI) +} + +// DistributionKeeper defines a subset of methods implemented by the cosmos-sdk distribution keeper +type DistributionKeeper interface { + DelegationRewards(c context.Context, req *types.QueryDelegationRewardsRequest) (*types.QueryDelegationRewardsResponse, error) +} + +// StakingKeeper defines a subset of methods implemented by the cosmos-sdk staking keeper +type StakingKeeper interface { + // BondDenom - Bondable coin denomination + BondDenom(ctx sdk.Context) (res string) + // GetValidator get a single validator + GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool) + // GetBondedValidatorsByPower get the current group of bonded validators sorted by power-rank + GetBondedValidatorsByPower(ctx sdk.Context) []stakingtypes.Validator + // GetAllDelegatorDelegations return all delegations for a delegator + GetAllDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress) []stakingtypes.Delegation + // GetDelegation return a specific delegation + GetDelegation(ctx sdk.Context, + delAddr sdk.AccAddress, valAddr sdk.ValAddress) (delegation stakingtypes.Delegation, found bool) + // HasReceivingRedelegation check if validator is receiving a redelegation + HasReceivingRedelegation(ctx sdk.Context, + delAddr sdk.AccAddress, valDstAddr sdk.ValAddress) bool +} + +// ChannelKeeper defines the expected IBC channel keeper +type ChannelKeeper interface { + GetChannel(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) + GetNextSequenceSend(ctx sdk.Context, portID, channelID string) (uint64, bool) + SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error + ChanCloseInit(ctx sdk.Context, portID, channelID string, chanCap *capabilitytypes.Capability) error + GetAllChannels(ctx sdk.Context) (channels []channeltypes.IdentifiedChannel) + IterateChannels(ctx sdk.Context, cb func(channeltypes.IdentifiedChannel) bool) + SetChannel(ctx sdk.Context, portID, channelID string, channel channeltypes.Channel) +} + +// ClientKeeper defines the expected IBC client keeper +type ClientKeeper interface { + GetClientConsensusState(ctx sdk.Context, clientID string) (connection ibcexported.ConsensusState, found bool) +} + +// ConnectionKeeper defines the expected IBC connection keeper +type ConnectionKeeper interface { + GetConnection(ctx sdk.Context, connectionID string) (connection connectiontypes.ConnectionEnd, found bool) +} + +// PortKeeper defines the expected IBC port keeper +type PortKeeper interface { + BindPort(ctx sdk.Context, portID string) *capabilitytypes.Capability +} + +type CapabilityKeeper interface { + GetCapability(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) + ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error + AuthenticateCapability(ctx sdk.Context, capability *capabilitytypes.Capability, name string) bool +} + +// ICS20TransferPortSource is a subset of the ibc transfer keeper. +type ICS20TransferPortSource interface { + GetPort(ctx sdk.Context) string +} diff --git a/x/wasm/types/exported_keepers.go b/x/wasm/types/exported_keepers.go new file mode 100644 index 00000000..dcf97cb2 --- /dev/null +++ b/x/wasm/types/exported_keepers.go @@ -0,0 +1,119 @@ +package types + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +// ViewKeeper provides read only operations +type ViewKeeper interface { + GetContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress) []ContractCodeHistoryEntry + QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) + QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte + HasContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) bool + GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *ContractInfo + IterateContractInfo(ctx sdk.Context, cb func(sdk.AccAddress, ContractInfo) bool) + IterateContractsByCreator(ctx sdk.Context, creator sdk.AccAddress, cb func(address sdk.AccAddress) bool) + IterateContractsByCode(ctx sdk.Context, codeID uint64, cb func(address sdk.AccAddress) bool) + IterateContractState(ctx sdk.Context, contractAddress sdk.AccAddress, cb func(key, value []byte) bool) + GetCodeInfo(ctx sdk.Context, codeID uint64) *CodeInfo + IterateCodeInfos(ctx sdk.Context, cb func(uint64, CodeInfo) bool) + GetByteCode(ctx sdk.Context, codeID uint64) ([]byte, error) + IsPinnedCode(ctx sdk.Context, codeID uint64) bool + GetParams(ctx sdk.Context) Params +} + +// ContractOpsKeeper contains mutable operations on a contract. +type ContractOpsKeeper interface { + // Create uploads and compiles a WASM contract, returning a short identifier for the contract + Create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, instantiateAccess *AccessConfig) (codeID uint64, checksum []byte, err error) + + // Instantiate creates an instance of a WASM contract using the classic sequence based address generator + Instantiate( + ctx sdk.Context, + codeID uint64, + creator, admin sdk.AccAddress, + initMsg []byte, + label string, + deposit sdk.Coins, + ) (sdk.AccAddress, []byte, error) + + // Instantiate2 creates an instance of a WASM contract using the predictable address generator + Instantiate2( + ctx sdk.Context, + codeID uint64, + creator, admin sdk.AccAddress, + initMsg []byte, + label string, + deposit sdk.Coins, + salt []byte, + fixMsg bool, + ) (sdk.AccAddress, []byte, error) + + // Execute executes the contract instance + Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) + + // Migrate allows to upgrade a contract to a new code with data migration. + Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) ([]byte, error) + + // Sudo allows to call privileged entry point of a contract. + Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error) + + // UpdateContractAdmin sets the admin value on the ContractInfo. It must be a valid address (use ClearContractAdmin to remove it) + UpdateContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newAdmin sdk.AccAddress) error + + // ClearContractAdmin sets the admin value on the ContractInfo to nil, to disable further migrations/ updates. + ClearContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress) error + + // PinCode pins the wasm contract in wasmvm cache + PinCode(ctx sdk.Context, codeID uint64) error + + // UnpinCode removes the wasm contract from wasmvm cache + UnpinCode(ctx sdk.Context, codeID uint64) error + + // SetContractInfoExtension updates the extension point data that is stored with the contract info + SetContractInfoExtension(ctx sdk.Context, contract sdk.AccAddress, extra ContractInfoExtension) error + + // SetAccessConfig updates the access config of a code id. + SetAccessConfig(ctx sdk.Context, codeID uint64, caller sdk.AccAddress, newConfig AccessConfig) error +} + +// IBCContractKeeper IBC lifecycle event handler +type IBCContractKeeper interface { + OnOpenChannel( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCChannelOpenMsg, + ) (string, error) + OnConnectChannel( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCChannelConnectMsg, + ) error + OnCloseChannel( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCChannelCloseMsg, + ) error + OnRecvPacket( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCPacketReceiveMsg, + ) ([]byte, error) + OnAckPacket( + ctx sdk.Context, + contractAddr sdk.AccAddress, + acknowledgement wasmvmtypes.IBCPacketAckMsg, + ) error + OnTimeoutPacket( + ctx sdk.Context, + contractAddr sdk.AccAddress, + msg wasmvmtypes.IBCPacketTimeoutMsg, + ) error + // ClaimCapability allows the transfer module to claim a capability + // that IBC module passes to it + ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error + // AuthenticateCapability wraps the scopedKeeper's AuthenticateCapability function + AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) bool +} diff --git a/x/wasm/types/feature_flag.go b/x/wasm/types/feature_flag.go new file mode 100644 index 00000000..959252c7 --- /dev/null +++ b/x/wasm/types/feature_flag.go @@ -0,0 +1,4 @@ +package types + +// Tests should not fail on gas consumption +const EnableGasVerification = true diff --git a/x/wasm/types/genesis.go b/x/wasm/types/genesis.go new file mode 100644 index 00000000..f14dbc3d --- /dev/null +++ b/x/wasm/types/genesis.go @@ -0,0 +1,102 @@ +package types + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func (s Sequence) ValidateBasic() error { + if len(s.IDKey) == 0 { + return sdkerrors.Wrap(ErrEmpty, "id key") + } + return nil +} + +func (s GenesisState) ValidateBasic() error { + if err := s.Params.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "params") + } + for i := range s.Codes { + if err := s.Codes[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "code: %d", i) + } + } + for i := range s.Contracts { + if err := s.Contracts[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "contract: %d", i) + } + } + for i := range s.Sequences { + if err := s.Sequences[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "sequence: %d", i) + } + } + + return nil +} + +func (c Code) ValidateBasic() error { + if c.CodeID == 0 { + return sdkerrors.Wrap(ErrEmpty, "code id") + } + if err := c.CodeInfo.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "code info") + } + if err := validateWasmCode(c.CodeBytes, MaxProposalWasmSize); err != nil { + return sdkerrors.Wrap(err, "code bytes") + } + return nil +} + +func (c Contract) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(c.ContractAddress); err != nil { + return sdkerrors.Wrap(err, "contract address") + } + if err := c.ContractInfo.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "contract info") + } + + if c.ContractInfo.Created == nil { + return sdkerrors.Wrap(ErrInvalid, "created must not be empty") + } + for i := range c.ContractState { + if err := c.ContractState[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "contract state %d", i) + } + } + if len(c.ContractCodeHistory) == 0 { + return ErrEmpty.Wrap("code history") + } + for i, v := range c.ContractCodeHistory { + if err := v.ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "code history element %d", i) + } + } + return nil +} + +// ValidateGenesis performs basic validation of supply genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.ValidateBasic() +} + +var _ codectypes.UnpackInterfacesMessage = GenesisState{} + +// UnpackInterfaces implements codectypes.UnpackInterfaces +func (s GenesisState) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + for _, v := range s.Contracts { + if err := v.UnpackInterfaces(unpacker); err != nil { + return err + } + } + return nil +} + +var _ codectypes.UnpackInterfacesMessage = &Contract{} + +// UnpackInterfaces implements codectypes.UnpackInterfaces +func (c *Contract) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + return c.ContractInfo.UnpackInterfaces(unpacker) +} diff --git a/x/wasm/types/genesis_test.go b/x/wasm/types/genesis_test.go new file mode 100644 index 00000000..51c95e4d --- /dev/null +++ b/x/wasm/types/genesis_test.go @@ -0,0 +1,211 @@ +package types + +import ( + "bytes" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/libs/rand" + + "github.com/stretchr/testify/require" +) + +func TestValidateGenesisState(t *testing.T) { + specs := map[string]struct { + srcMutator func(*GenesisState) + expError bool + }{ + "all good": { + srcMutator: func(s *GenesisState) {}, + }, + "params invalid": { + srcMutator: func(s *GenesisState) { + s.Params = Params{} + }, + expError: true, + }, + "codeinfo invalid": { + srcMutator: func(s *GenesisState) { + s.Codes[0].CodeInfo.CodeHash = nil + }, + expError: true, + }, + "contract invalid": { + srcMutator: func(s *GenesisState) { + s.Contracts[0].ContractAddress = "invalid" + }, + expError: true, + }, + "sequence invalid": { + srcMutator: func(s *GenesisState) { + s.Sequences[0].IDKey = nil + }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := GenesisFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestCodeValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*Code) + expError bool + }{ + "all good": {srcMutator: func(_ *Code) {}}, + "code id invalid": { + srcMutator: func(c *Code) { + c.CodeID = 0 + }, + expError: true, + }, + "codeinfo invalid": { + srcMutator: func(c *Code) { + c.CodeInfo.CodeHash = nil + }, + expError: true, + }, + "codeBytes empty": { + srcMutator: func(c *Code) { + c.CodeBytes = []byte{} + }, + expError: true, + }, + "codeBytes nil": { + srcMutator: func(c *Code) { + c.CodeBytes = nil + }, + expError: true, + }, + "codeBytes greater limit": { + srcMutator: func(c *Code) { + c.CodeBytes = bytes.Repeat([]byte{0x1}, MaxProposalWasmSize+1) + }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := CodeFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestContractValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*Contract) + expError bool + }{ + "all good": {srcMutator: func(_ *Contract) {}}, + "contract address invalid": { + srcMutator: func(c *Contract) { + c.ContractAddress = "invalid" + }, + expError: true, + }, + "contract info invalid": { + srcMutator: func(c *Contract) { + c.ContractInfo.Creator = "invalid" + }, + expError: true, + }, + "contract with created set": { + srcMutator: func(c *Contract) { + c.ContractInfo.Created = &AbsoluteTxPosition{} + }, + expError: false, + }, + "contract state invalid": { + srcMutator: func(c *Contract) { + c.ContractState = append(c.ContractState, Model{}) + }, + expError: true, + }, + "contract history invalid": { + srcMutator: func(c *Contract) { + c.ContractCodeHistory = []ContractCodeHistoryEntry{{}} + }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := ContractFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestGenesisContractInfoMarshalUnmarshal(t *testing.T) { + var myAddr sdk.AccAddress = rand.Bytes(ContractAddrLen) + var myOtherAddr sdk.AccAddress = rand.Bytes(ContractAddrLen) + anyPos := AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2} + + anyTime := time.Now().UTC() + // using gov proposal here as a random protobuf types as it contains an Any type inside for nested unpacking + myExtension, err := govtypes.NewProposal(&govtypes.TextProposal{Title: "bar"}, 1, anyTime, anyTime) + require.NoError(t, err) + myExtension.TotalDeposit = nil + + src := NewContractInfo(1, myAddr, myOtherAddr, "bar", &anyPos) + err = src.SetExtension(&myExtension) + require.NoError(t, err) + + interfaceRegistry := types.NewInterfaceRegistry() + marshaler := codec.NewProtoCodec(interfaceRegistry) + RegisterInterfaces(interfaceRegistry) + // register proposal as extension type + interfaceRegistry.RegisterImplementations( + (*ContractInfoExtension)(nil), + &govtypes.Proposal{}, + ) + // register gov types for nested Anys + govtypes.RegisterInterfaces(interfaceRegistry) + + // when encode + gs := GenesisState{ + Contracts: []Contract{{ + ContractInfo: src, + }}, + } + + bz, err := marshaler.Marshal(&gs) + require.NoError(t, err) + // and decode + var destGs GenesisState + err = marshaler.Unmarshal(bz, &destGs) + require.NoError(t, err) + // then + require.Len(t, destGs.Contracts, 1) + dest := destGs.Contracts[0].ContractInfo + assert.Equal(t, src, dest) + // and sanity check nested any + var destExt govtypes.Proposal + require.NoError(t, dest.ReadExtension(&destExt)) + assert.Equal(t, destExt.GetTitle(), "bar") +} diff --git a/x/wasm/types/iavl_range_test.go b/x/wasm/types/iavl_range_test.go new file mode 100644 index 00000000..85a4a18c --- /dev/null +++ b/x/wasm/types/iavl_range_test.go @@ -0,0 +1,83 @@ +package types + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/store/iavl" + iavl2 "github.com/cosmos/iavl" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" +) + +// This is modeled close to +// https://github.com/CosmWasm/cosmwasm-plus/blob/f97a7de44b6a930fd1d5179ee6f95b786a532f32/packages/storage-plus/src/prefix.rs#L183 +// and designed to ensure the IAVL store handles bounds the same way as the mock storage we use in Rust contract tests +func TestIavlRangeBounds(t *testing.T) { + memdb := dbm.NewMemDB() + tree, err := iavl2.NewMutableTree(memdb, 50, false) + require.NoError(t, err) + kvstore := iavl.UnsafeNewStore(tree) + + // values to compare with + expected := []KV{ + {[]byte("bar"), []byte("1")}, + {[]byte("ra"), []byte("2")}, + {[]byte("zi"), []byte("3")}, + } + reversed := []KV{ + {[]byte("zi"), []byte("3")}, + {[]byte("ra"), []byte("2")}, + {[]byte("bar"), []byte("1")}, + } + + // set up test cases, like `ensure_proper_range_bounds` in `cw-storage-plus` + for _, kv := range expected { + kvstore.Set(kv.Key, kv.Value) + } + + cases := map[string]struct { + start []byte + end []byte + reverse bool + expected []KV + }{ + "all ascending": {nil, nil, false, expected}, + "ascending start inclusive": {[]byte("ra"), nil, false, expected[1:]}, + "ascending end exclusive": {nil, []byte("ra"), false, expected[:1]}, + "ascending both points": {[]byte("bar"), []byte("zi"), false, expected[:2]}, + + "all descending": {nil, nil, true, reversed}, + "descending start inclusive": {[]byte("ra"), nil, true, reversed[:2]}, // "zi", "ra" + "descending end inclusive": {nil, []byte("ra"), true, reversed[2:]}, // "bar" + "descending both points": {[]byte("bar"), []byte("zi"), true, reversed[1:]}, // "ra", "bar" + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var iter store.Iterator + if tc.reverse { + iter = kvstore.ReverseIterator(tc.start, tc.end) + } else { + iter = kvstore.Iterator(tc.start, tc.end) + } + items := consume(iter) + require.Equal(t, tc.expected, items) + iter.Close() + }) + } +} + +type KV struct { + Key []byte + Value []byte +} + +func consume(itr store.Iterator) []KV { + var res []KV + for ; itr.Valid(); itr.Next() { + k, v := itr.Key(), itr.Value() + res = append(res, KV{k, v}) + } + return res +} diff --git a/x/wasm/types/json_matching.go b/x/wasm/types/json_matching.go new file mode 100644 index 00000000..34ff76d3 --- /dev/null +++ b/x/wasm/types/json_matching.go @@ -0,0 +1,34 @@ +package types + +import ( + "encoding/json" +) + +// isJSONObjectWithTopLevelKey returns true if the given bytes are a valid JSON object +// with exactly one top-level key that is contained in the list of allowed keys. +func isJSONObjectWithTopLevelKey(jsonBytes RawContractMessage, allowedKeys []string) (bool, error) { + if err := jsonBytes.ValidateBasic(); err != nil { + return false, err + } + + document := map[string]interface{}{} + if err := json.Unmarshal(jsonBytes, &document); err != nil { + return false, nil // not a map + } + + if len(document) != 1 { + return false, nil // unsupported type + } + + // Loop is executed exactly once + for topLevelKey := range document { + for _, allowedKey := range allowedKeys { + if allowedKey == topLevelKey { + return true, nil + } + } + return false, nil + } + + panic("Reached unreachable code. This is a bug.") +} diff --git a/x/wasm/types/json_matching_test.go b/x/wasm/types/json_matching_test.go new file mode 100644 index 00000000..01d2d3ef --- /dev/null +++ b/x/wasm/types/json_matching_test.go @@ -0,0 +1,134 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + // sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +func TestIsJSONObjectWithTopLevelKey(t *testing.T) { + specs := map[string]struct { + src []byte + allowedKeys []string + expResult bool + expErr error + }{ + "happy": { + src: []byte(`{"msg": {"foo":"bar"}}`), + allowedKeys: []string{"msg"}, + expResult: true, + }, + "happy with many allowed keys 1": { + src: []byte(`{"claim": {"foo":"bar"}}`), + allowedKeys: []string{"claim", "swap", "burn", "mint"}, + expResult: true, + }, + "happy with many allowed keys 2": { + src: []byte(`{"burn": {"foo":"bar"}}`), + allowedKeys: []string{"claim", "swap", "burn", "mint"}, + expResult: true, + }, + "happy with many allowed keys 3": { + src: []byte(`{"mint": {"foo":"bar"}}`), + allowedKeys: []string{"claim", "swap", "burn", "mint"}, + expResult: true, + }, + "happy with number": { + src: []byte(`{"msg": 123}`), + allowedKeys: []string{"msg"}, + expResult: true, + }, + "happy with array": { + src: []byte(`{"msg": [1, 2, 3, 4]}`), + allowedKeys: []string{"msg"}, + expResult: true, + }, + "happy with null": { + src: []byte(`{"msg": null}`), + allowedKeys: []string{"msg"}, + expResult: true, + }, + "happy with whitespace": { + src: []byte(`{ + "msg": null }`), + allowedKeys: []string{"msg"}, + expResult: true, + }, + "happy with escaped key": { + src: []byte(`{"event\u2468thing": {"foo":"bar"}}`), + allowedKeys: []string{"event⑨thing"}, + expResult: true, + }, + + // Invalid JSON object + "errors for bytes that are no JSON": { + src: []byte(`nope`), + allowedKeys: []string{"claim"}, + expErr: ErrInvalid, + }, + "false for valid JSON (string)": { + src: []byte(`"nope"`), + allowedKeys: []string{"claim"}, + expResult: false, + }, + "false for valid JSON (array)": { + src: []byte(`[1, 2, 3]`), + allowedKeys: []string{"claim"}, + expResult: false, + }, + // not supported: https://github.com/golang/go/issues/24415 + //"errors for duplicate key": { + // src: []byte(`{"claim": "foo", "claim":"bar"}`), + // allowedKeys: []string{"claim"}, + // expErr: ErrNotAJSONObject, + //}, + + // Not one top-level key + "false for no top-level key": { + src: []byte(`{}`), + allowedKeys: []string{"claim"}, + expResult: false, + }, + "false for multiple top-level keys": { + src: []byte(`{"claim": {}, "and_swap": {}}`), + allowedKeys: []string{"claim"}, + expResult: false, + }, + + // Wrong top-level key + "wrong top-level key 1": { + src: []byte(`{"claim": {}}`), + allowedKeys: []string{""}, + expResult: false, + }, + "wrong top-level key 2": { + src: []byte(`{"claim": {}}`), + allowedKeys: []string{"swap", "burn", "mint"}, + expResult: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + exists, gotErr := isJSONObjectWithTopLevelKey(spec.src, spec.allowedKeys) + if spec.expErr != nil { + assert.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expResult, exists) + }) + } +} + +func TestDuplicateKeyGivesSameResult(t *testing.T) { + jsonBytes := []byte(`{"event⑨thing": "foo", "event⑨thing":"bar"}`) + for i := 0; i < 10000; i++ { + document := map[string]interface{}{} + require.NoError(t, json.Unmarshal(jsonBytes, &document)) + assert.Equal(t, "bar", document["event⑨thing"]) + } +} diff --git a/x/wasm/types/keys.go b/x/wasm/types/keys.go new file mode 100644 index 00000000..95078e14 --- /dev/null +++ b/x/wasm/types/keys.go @@ -0,0 +1,131 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" +) + +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 wasm module + QuerierRoute = ModuleName + + // RouterKey is the msg router key for the wasm module + RouterKey = ModuleName +) + +// nolint +var ( + CodeKeyPrefix = []byte{0x01} + ContractKeyPrefix = []byte{0x02} + ContractStorePrefix = []byte{0x03} + SequenceKeyPrefix = []byte{0x04} + ContractCodeHistoryElementPrefix = []byte{0x05} + ContractByCodeIDAndCreatedSecondaryIndexPrefix = []byte{0x06} + PinnedCodeIndexPrefix = []byte{0x07} + TXCounterPrefix = []byte{0x08} + ContractsByCreatorPrefix = []byte{0x09} + + KeyLastCodeID = append(SequenceKeyPrefix, []byte("lastCodeId")...) + KeyLastInstanceID = append(SequenceKeyPrefix, []byte("lastContractId")...) +) + +// GetCodeKey constructs the key for retreiving the ID for the WASM code +func GetCodeKey(codeID uint64) []byte { + contractIDBz := sdk.Uint64ToBigEndian(codeID) + return append(CodeKeyPrefix, contractIDBz...) +} + +// GetContractAddressKey returns the key for the WASM contract instance +func GetContractAddressKey(addr sdk.AccAddress) []byte { + return append(ContractKeyPrefix, addr...) +} + +// GetContractsByCreatorPrefix returns the contracts by creator prefix for the WASM contract instance +func GetContractsByCreatorPrefix(addr sdk.AccAddress) []byte { + bz := address.MustLengthPrefix(addr) + return append(ContractsByCreatorPrefix, bz...) +} + +// GetContractStorePrefix returns the store prefix for the WASM contract instance +func GetContractStorePrefix(addr sdk.AccAddress) []byte { + return append(ContractStorePrefix, addr...) +} + +// GetContractByCreatedSecondaryIndexKey returns the key for the secondary index: +// `` +func GetContractByCreatedSecondaryIndexKey(contractAddr sdk.AccAddress, c ContractCodeHistoryEntry) []byte { + prefix := GetContractByCodeIDSecondaryIndexPrefix(c.CodeID) + prefixLen := len(prefix) + contractAddrLen := len(contractAddr) + r := make([]byte, prefixLen+AbsoluteTxPositionLen+contractAddrLen) + copy(r[0:], prefix) + copy(r[prefixLen:], c.Updated.Bytes()) + copy(r[prefixLen+AbsoluteTxPositionLen:], contractAddr) + return r +} + +// GetContractByCodeIDSecondaryIndexPrefix returns the prefix for the second index: `` +func GetContractByCodeIDSecondaryIndexPrefix(codeID uint64) []byte { + prefixLen := len(ContractByCodeIDAndCreatedSecondaryIndexPrefix) + const codeIDLen = 8 + r := make([]byte, prefixLen+codeIDLen) + copy(r[0:], ContractByCodeIDAndCreatedSecondaryIndexPrefix) + copy(r[prefixLen:], sdk.Uint64ToBigEndian(codeID)) + return r +} + +// GetContractByCreatorSecondaryIndexKey returns the key for the second index: `` +func GetContractByCreatorSecondaryIndexKey(bz []byte, position []byte, contractAddr sdk.AccAddress) []byte { + prefixBytes := GetContractsByCreatorPrefix(bz) + lenPrefixBytes := len(prefixBytes) + r := make([]byte, lenPrefixBytes+AbsoluteTxPositionLen+len(contractAddr)) + + copy(r[:lenPrefixBytes], prefixBytes) + copy(r[lenPrefixBytes:lenPrefixBytes+AbsoluteTxPositionLen], position) + copy(r[lenPrefixBytes+AbsoluteTxPositionLen:], contractAddr) + + return r +} + +// GetContractCodeHistoryElementKey returns the key a contract code history entry: `` +func GetContractCodeHistoryElementKey(contractAddr sdk.AccAddress, pos uint64) []byte { + prefix := GetContractCodeHistoryElementPrefix(contractAddr) + prefixLen := len(prefix) + r := make([]byte, prefixLen+8) + copy(r[0:], prefix) + copy(r[prefixLen:], sdk.Uint64ToBigEndian(pos)) + return r +} + +// GetContractCodeHistoryElementPrefix returns the key prefix for a contract code history entry: `` +func GetContractCodeHistoryElementPrefix(contractAddr sdk.AccAddress) []byte { + prefixLen := len(ContractCodeHistoryElementPrefix) + contractAddrLen := len(contractAddr) + r := make([]byte, prefixLen+contractAddrLen) + copy(r[0:], ContractCodeHistoryElementPrefix) + copy(r[prefixLen:], contractAddr) + return r +} + +// GetPinnedCodeIndexPrefix returns the key prefix for a code id pinned into the wasmvm cache +func GetPinnedCodeIndexPrefix(codeID uint64) []byte { + prefixLen := len(PinnedCodeIndexPrefix) + r := make([]byte, prefixLen+8) + copy(r[0:], PinnedCodeIndexPrefix) + copy(r[prefixLen:], sdk.Uint64ToBigEndian(codeID)) + return r +} + +// ParsePinnedCodeIndex converts the serialized code ID back. +func ParsePinnedCodeIndex(s []byte) uint64 { + return sdk.BigEndianToUint64(s) +} diff --git a/x/wasm/types/keys_test.go b/x/wasm/types/keys_test.go new file mode 100644 index 00000000..3db6bbad --- /dev/null +++ b/x/wasm/types/keys_test.go @@ -0,0 +1,147 @@ +package types + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetContractByCodeIDSecondaryIndexPrefix(t *testing.T) { + specs := map[string]struct { + src uint64 + exp []byte + }{ + "small number": { + src: 1, + exp: []byte{6, 0, 0, 0, 0, 0, 0, 0, 1}, + }, + "big number": { + src: 1 << (8 * 7), + exp: []byte{6, 1, 0, 0, 0, 0, 0, 0, 0}, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got := GetContractByCodeIDSecondaryIndexPrefix(spec.src) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestGetContractCodeHistoryElementPrefix(t *testing.T) { + // test that contract addresses of 20 length are still supported + addr := bytes.Repeat([]byte{4}, 20) + got := GetContractCodeHistoryElementPrefix(addr) + exp := []byte{ + 5, // prefix + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 20 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + } + assert.Equal(t, exp, got) + + addr = bytes.Repeat([]byte{4}, ContractAddrLen) + got = GetContractCodeHistoryElementPrefix(addr) + exp = []byte{ + 5, // prefix + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 32 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, + } + assert.Equal(t, exp, got) +} + +func TestGetContractByCreatedSecondaryIndexKey(t *testing.T) { + e := ContractCodeHistoryEntry{ + CodeID: 1, + Updated: &AbsoluteTxPosition{2 + 1<<(8*7), 3 + 1<<(8*7)}, + } + + // test that contract addresses of 20 length are still supported + addr := bytes.Repeat([]byte{4}, 20) + got := GetContractByCreatedSecondaryIndexKey(addr, e) + exp := []byte{ + 6, // prefix + 0, 0, 0, 0, 0, 0, 0, 1, // codeID + 1, 0, 0, 0, 0, 0, 0, 2, // height + 1, 0, 0, 0, 0, 0, 0, 3, // index + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 32 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + } + assert.Equal(t, exp, got) + + addr = bytes.Repeat([]byte{4}, ContractAddrLen) + got = GetContractByCreatedSecondaryIndexKey(addr, e) + exp = []byte{ + 6, // prefix + 0, 0, 0, 0, 0, 0, 0, 1, // codeID + 1, 0, 0, 0, 0, 0, 0, 2, // height + 1, 0, 0, 0, 0, 0, 0, 3, // index + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 32 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, + } + assert.Equal(t, exp, got) +} + +func TestGetContractByCreatorSecondaryIndexKey(t *testing.T) { + creatorAddr := bytes.Repeat([]byte{4}, 20) + e := ContractCodeHistoryEntry{ + CodeID: 1, + Updated: &AbsoluteTxPosition{2 + 1<<(8*7), 3 + 1<<(8*7)}, + } + + // test that contract addresses of 20 length are still supported + contractAddr := bytes.Repeat([]byte{4}, 20) + got := GetContractByCreatorSecondaryIndexKey(creatorAddr, e.Updated.Bytes(), contractAddr) + exp := []byte{ + 9, // prefix + 20, // creator address length + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // creator address with fixed length prefix + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 1, 0, 0, 0, 0, 0, 0, 2, // height + 1, 0, 0, 0, 0, 0, 0, 3, // index + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 20 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + } + assert.Equal(t, exp, got) + + // test that contract addresses of 32 length are still supported + contractAddr = bytes.Repeat([]byte{4}, 32) + got = GetContractByCreatorSecondaryIndexKey(creatorAddr, e.Updated.Bytes(), contractAddr) + exp = []byte{ + 9, // prefix + 20, // creator address length + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // creator address with fixed length prefix + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 1, 0, 0, 0, 0, 0, 0, 2, // height + 1, 0, 0, 0, 0, 0, 0, 3, // index + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 32 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, + } + + // test that creator is contract addresses of 32 length + contractAddr = bytes.Repeat([]byte{4}, 32) + creatorAddr = bytes.Repeat([]byte{8}, 32) + got = GetContractByCreatorSecondaryIndexKey(creatorAddr, e.Updated.Bytes(), contractAddr) + exp = []byte{ + 9, // prefix + 32, // creator address length + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // creator address is a contract address + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, + 1, 0, 0, 0, 0, 0, 0, 2, // height + 1, 0, 0, 0, 0, 0, 0, 3, // index + + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // address 32 bytes + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, + } + assert.Equal(t, exp, got) +} diff --git a/x/wasm/types/params.go b/x/wasm/types/params.go new file mode 100644 index 00000000..0ee7dcb6 --- /dev/null +++ b/x/wasm/types/params.go @@ -0,0 +1,227 @@ +package types + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/gogo/protobuf/jsonpb" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +var ( + ParamStoreKeyUploadAccess = []byte("uploadAccess") + ParamStoreKeyInstantiateAccess = []byte("instantiateAccess") +) + +var AllAccessTypes = []AccessType{ + AccessTypeNobody, + AccessTypeOnlyAddress, + AccessTypeAnyOfAddresses, + AccessTypeEverybody, +} + +func (a AccessType) With(addrs ...sdk.AccAddress) AccessConfig { + switch a { + case AccessTypeNobody: + return AllowNobody + case AccessTypeOnlyAddress: + if n := len(addrs); n != 1 { + panic(fmt.Sprintf("expected exactly 1 address but got %d", n)) + } + if err := sdk.VerifyAddressFormat(addrs[0]); err != nil { + panic(err) + } + return AccessConfig{Permission: AccessTypeOnlyAddress, Address: addrs[0].String()} + case AccessTypeEverybody: + return AllowEverybody + case AccessTypeAnyOfAddresses: + bech32Addrs := make([]string, len(addrs)) + for i, v := range addrs { + bech32Addrs[i] = v.String() + } + if err := assertValidAddresses(bech32Addrs); err != nil { + panic(sdkerrors.Wrap(err, "addresses")) + } + return AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: bech32Addrs} + } + panic("unsupported access type") +} + +func (a AccessType) String() string { + switch a { + case AccessTypeNobody: + return "Nobody" + case AccessTypeOnlyAddress: + return "OnlyAddress" + case AccessTypeEverybody: + return "Everybody" + case AccessTypeAnyOfAddresses: + return "AnyOfAddresses" + } + return "Unspecified" +} + +func (a *AccessType) UnmarshalText(text []byte) error { + for _, v := range AllAccessTypes { + if v.String() == string(text) { + *a = v + return nil + } + } + *a = AccessTypeUnspecified + return nil +} + +func (a AccessType) MarshalText() ([]byte, error) { + return []byte(a.String()), nil +} + +func (a *AccessType) MarshalJSONPB(_ *jsonpb.Marshaler) ([]byte, error) { + return json.Marshal(a) +} + +func (a *AccessType) UnmarshalJSONPB(_ *jsonpb.Unmarshaler, data []byte) error { + return json.Unmarshal(data, a) +} + +func (a AccessConfig) Equals(o AccessConfig) bool { + return a.Permission == o.Permission && a.Address == o.Address +} + +var ( + DefaultUploadAccess = AllowEverybody + AllowEverybody = AccessConfig{Permission: AccessTypeEverybody} + AllowNobody = AccessConfig{Permission: AccessTypeNobody} +) + +// ParamKeyTable returns the parameter key table. +func ParamKeyTable() paramtypes.KeyTable { + return paramtypes.NewKeyTable().RegisterParamSet(&Params{}) +} + +// DefaultParams returns default wasm parameters +func DefaultParams() Params { + return Params{ + CodeUploadAccess: AllowEverybody, + InstantiateDefaultPermission: AccessTypeEverybody, + } +} + +func (p Params) String() string { + out, err := yaml.Marshal(p) + if err != nil { + panic(err) + } + return string(out) +} + +// ParamSetPairs returns the parameter set pairs. +func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { + return paramtypes.ParamSetPairs{ + paramtypes.NewParamSetPair(ParamStoreKeyUploadAccess, &p.CodeUploadAccess, validateAccessConfig), + paramtypes.NewParamSetPair(ParamStoreKeyInstantiateAccess, &p.InstantiateDefaultPermission, validateAccessType), + } +} + +// ValidateBasic performs basic validation on wasm parameters +func (p Params) ValidateBasic() error { + if err := validateAccessType(p.InstantiateDefaultPermission); err != nil { + return errors.Wrap(err, "instantiate default permission") + } + if err := validateAccessConfig(p.CodeUploadAccess); err != nil { + return errors.Wrap(err, "upload access") + } + return nil +} + +func validateAccessConfig(i interface{}) error { + v, ok := i.(AccessConfig) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + return v.ValidateBasic() +} + +func validateAccessType(i interface{}) error { + a, ok := i.(AccessType) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + if a == AccessTypeUnspecified { + return sdkerrors.Wrap(ErrEmpty, "type") + } + for _, v := range AllAccessTypes { + if v == a { + return nil + } + } + return sdkerrors.Wrapf(ErrInvalid, "unknown type: %q", a) +} + +// ValidateBasic performs basic validation +func (a AccessConfig) ValidateBasic() error { + switch a.Permission { + case AccessTypeUnspecified: + return sdkerrors.Wrap(ErrEmpty, "type") + case AccessTypeNobody, AccessTypeEverybody: + if len(a.Address) != 0 { + return sdkerrors.Wrap(ErrInvalid, "address not allowed for this type") + } + return nil + case AccessTypeOnlyAddress: + if len(a.Addresses) != 0 { + return ErrInvalid.Wrap("addresses field set") + } + _, err := sdk.AccAddressFromBech32(a.Address) + return err + case AccessTypeAnyOfAddresses: + if a.Address != "" { + return ErrInvalid.Wrap("address field set") + } + return sdkerrors.Wrap(assertValidAddresses(a.Addresses), "addresses") + } + return sdkerrors.Wrapf(ErrInvalid, "unknown type: %q", a.Permission) +} + +func assertValidAddresses(addrs []string) error { + if len(addrs) == 0 { + return ErrEmpty + } + idx := make(map[string]struct{}, len(addrs)) + for _, a := range addrs { + if _, err := sdk.AccAddressFromBech32(a); err != nil { + return sdkerrors.Wrapf(err, "address: %s", a) + } + if _, exists := idx[a]; exists { + return ErrDuplicate.Wrapf("address: %s", a) + } + idx[a] = struct{}{} + } + return nil +} + +// Allowed returns if permission includes the actor. +// Actor address must be valid and not nil +func (a AccessConfig) Allowed(actor sdk.AccAddress) bool { + switch a.Permission { + case AccessTypeNobody: + return false + case AccessTypeEverybody: + return true + case AccessTypeOnlyAddress: + return a.Address == actor.String() + case AccessTypeAnyOfAddresses: + for _, v := range a.Addresses { + if v == actor.String() { + return true + } + } + return false + default: + panic("unknown type") + } +} diff --git a/x/wasm/types/params_test.go b/x/wasm/types/params_test.go new file mode 100644 index 00000000..77ac8b74 --- /dev/null +++ b/x/wasm/types/params_test.go @@ -0,0 +1,306 @@ +package types + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateParams(t *testing.T) { + var ( + anyAddress sdk.AccAddress = make([]byte, ContractAddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{1}, ContractAddrLen) + invalidAddress = "invalid address" + ) + + specs := map[string]struct { + src Params + expErr bool + }{ + "all good with defaults": { + src: DefaultParams(), + }, + "all good with nobody": { + src: Params{ + CodeUploadAccess: AllowNobody, + InstantiateDefaultPermission: AccessTypeNobody, + }, + }, + "all good with everybody": { + src: Params{ + CodeUploadAccess: AllowEverybody, + InstantiateDefaultPermission: AccessTypeEverybody, + }, + }, + "all good with only address": { + src: Params{ + CodeUploadAccess: AccessTypeOnlyAddress.With(anyAddress), + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + }, + "all good with anyOf address": { + src: Params{ + CodeUploadAccess: AccessTypeAnyOfAddresses.With(anyAddress), + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + }, + "all good with anyOf addresses": { + src: Params{ + CodeUploadAccess: AccessTypeAnyOfAddresses.With(anyAddress, otherAddress), + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + }, + "reject empty type in instantiate permission": { + src: Params{ + CodeUploadAccess: AllowNobody, + }, + expErr: true, + }, + "reject unknown type in instantiate": { + src: Params{ + CodeUploadAccess: AllowNobody, + InstantiateDefaultPermission: 1111, + }, + expErr: true, + }, + "reject invalid address in only address": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeOnlyAddress, Address: invalidAddress}, + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + expErr: true, + }, + "reject wrong field addresses in only address": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeOnlyAddress, Address: anyAddress.String(), Addresses: []string{anyAddress.String()}}, + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + expErr: true, + }, + "reject CodeUploadAccess Everybody with obsolete address": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeEverybody, Address: anyAddress.String()}, + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + expErr: true, + }, + "reject CodeUploadAccess Nobody with obsolete address": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeNobody, Address: anyAddress.String()}, + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + expErr: true, + }, + "reject empty CodeUploadAccess": { + src: Params{ + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + expErr: true, + }, + "reject undefined permission in CodeUploadAccess": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeUnspecified}, + InstantiateDefaultPermission: AccessTypeOnlyAddress, + }, + expErr: true, + }, + "reject empty addresses in any of addresses": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{}}, + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + expErr: true, + }, + "reject addresses not set in any of addresses": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeAnyOfAddresses}, + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + expErr: true, + }, + "reject invalid address in any of addresses": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{invalidAddress}}, + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + expErr: true, + }, + "reject duplicate address in any of addresses": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{anyAddress.String(), anyAddress.String()}}, + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + expErr: true, + }, + "reject wrong field address in any of addresses": { + src: Params{ + CodeUploadAccess: AccessConfig{Permission: AccessTypeAnyOfAddresses, Address: anyAddress.String(), Addresses: []string{anyAddress.String()}}, + InstantiateDefaultPermission: AccessTypeAnyOfAddresses, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAccessTypeMarshalJson(t *testing.T) { + specs := map[string]struct { + src AccessType + exp string + }{ + "Unspecified": {src: AccessTypeUnspecified, exp: `"Unspecified"`}, + "Nobody": {src: AccessTypeNobody, exp: `"Nobody"`}, + "OnlyAddress": {src: AccessTypeOnlyAddress, exp: `"OnlyAddress"`}, + "AccessTypeAnyOfAddresses": {src: AccessTypeAnyOfAddresses, exp: `"AnyOfAddresses"`}, + "Everybody": {src: AccessTypeEverybody, exp: `"Everybody"`}, + "unknown": {src: 999, exp: `"Unspecified"`}, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := json.Marshal(spec.src) + require.NoError(t, err) + assert.Equal(t, []byte(spec.exp), got) + }) + } +} + +func TestAccessTypeUnmarshalJson(t *testing.T) { + specs := map[string]struct { + src string + exp AccessType + }{ + "Unspecified": {src: `"Unspecified"`, exp: AccessTypeUnspecified}, + "Nobody": {src: `"Nobody"`, exp: AccessTypeNobody}, + "OnlyAddress": {src: `"OnlyAddress"`, exp: AccessTypeOnlyAddress}, + "AnyOfAddresses": {src: `"AnyOfAddresses"`, exp: AccessTypeAnyOfAddresses}, + "Everybody": {src: `"Everybody"`, exp: AccessTypeEverybody}, + "unknown": {src: `""`, exp: AccessTypeUnspecified}, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + var got AccessType + err := json.Unmarshal([]byte(spec.src), &got) + require.NoError(t, err) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestParamsUnmarshalJson(t *testing.T) { + specs := map[string]struct { + src string + exp Params + }{ + "defaults": { + src: `{"code_upload_access": {"permission": "Everybody"}, + "instantiate_default_permission": "Everybody"}`, + exp: DefaultParams(), + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + var val Params + interfaceRegistry := codectypes.NewInterfaceRegistry() + marshaler := codec.NewProtoCodec(interfaceRegistry) + + err := marshaler.UnmarshalJSON([]byte(spec.src), &val) + require.NoError(t, err) + assert.Equal(t, spec.exp, val) + }) + } +} + +func TestAccessTypeWith(t *testing.T) { + myAddress := sdk.AccAddress(randBytes(SDKAddrLen)) + myOtherAddress := sdk.AccAddress(randBytes(SDKAddrLen)) + specs := map[string]struct { + src AccessType + addrs []sdk.AccAddress + exp AccessConfig + expPanic bool + }{ + "nobody": { + src: AccessTypeNobody, + exp: AccessConfig{Permission: AccessTypeNobody}, + }, + "nobody with address": { + src: AccessTypeNobody, + addrs: []sdk.AccAddress{myAddress}, + exp: AccessConfig{Permission: AccessTypeNobody}, + }, + "everybody": { + src: AccessTypeEverybody, + exp: AccessConfig{Permission: AccessTypeEverybody}, + }, + "everybody with address": { + src: AccessTypeEverybody, + addrs: []sdk.AccAddress{myAddress}, + exp: AccessConfig{Permission: AccessTypeEverybody}, + }, + "only address without address": { + src: AccessTypeOnlyAddress, + expPanic: true, + }, + "only address with address": { + src: AccessTypeOnlyAddress, + addrs: []sdk.AccAddress{myAddress}, + exp: AccessConfig{Permission: AccessTypeOnlyAddress, Address: myAddress.String()}, + }, + "only address with invalid address": { + src: AccessTypeOnlyAddress, + addrs: []sdk.AccAddress{nil}, + expPanic: true, + }, + "any of address without address": { + src: AccessTypeAnyOfAddresses, + expPanic: true, + }, + "any of address with single address": { + src: AccessTypeAnyOfAddresses, + addrs: []sdk.AccAddress{myAddress}, + exp: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{myAddress.String()}}, + }, + "any of address with multiple addresses": { + src: AccessTypeAnyOfAddresses, + addrs: []sdk.AccAddress{myAddress, myOtherAddress}, + exp: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{myAddress.String(), myOtherAddress.String()}}, + }, + "any of address with duplicate addresses": { + src: AccessTypeAnyOfAddresses, + addrs: []sdk.AccAddress{myAddress, myAddress}, + expPanic: true, + }, + "any of address with invalid address": { + src: AccessTypeAnyOfAddresses, + addrs: []sdk.AccAddress{nil}, + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if !spec.expPanic { + got := spec.src.With(spec.addrs...) + assert.Equal(t, spec.exp, got) + return + } + assert.Panics(t, func() { + spec.src.With(spec.addrs...) + }) + }) + } +} diff --git a/x/wasm/types/proposal.go b/x/wasm/types/proposal.go new file mode 100644 index 00000000..96e2c9b5 --- /dev/null +++ b/x/wasm/types/proposal.go @@ -0,0 +1,996 @@ +package types + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +type ProposalType string + +const ( + ProposalTypeStoreCode ProposalType = "StoreCode" + ProposalTypeInstantiateContract ProposalType = "InstantiateContract" + ProposalTypeInstantiateContract2 ProposalType = "InstantiateContract2" + ProposalTypeMigrateContract ProposalType = "MigrateContract" + ProposalTypeSudoContract ProposalType = "SudoContract" + ProposalTypeExecuteContract ProposalType = "ExecuteContract" + ProposalTypeUpdateAdmin ProposalType = "UpdateAdmin" + ProposalTypeClearAdmin ProposalType = "ClearAdmin" + ProposalTypePinCodes ProposalType = "PinCodes" + ProposalTypeUnpinCodes ProposalType = "UnpinCodes" + ProposalTypeUpdateInstantiateConfig ProposalType = "UpdateInstantiateConfig" + ProposalTypeStoreAndInstantiateContractProposal ProposalType = "StoreAndInstantiateContract" +) + +// DisableAllProposals contains no wasm gov types. +var DisableAllProposals []ProposalType + +// EnableAllProposals contains all wasm gov types as keys. +var EnableAllProposals = []ProposalType{ + ProposalTypeStoreCode, + ProposalTypeInstantiateContract, + ProposalTypeInstantiateContract2, + ProposalTypeMigrateContract, + ProposalTypeSudoContract, + ProposalTypeExecuteContract, + ProposalTypeUpdateAdmin, + ProposalTypeClearAdmin, + ProposalTypePinCodes, + ProposalTypeUnpinCodes, + ProposalTypeUpdateInstantiateConfig, + ProposalTypeStoreAndInstantiateContractProposal, +} + +// ConvertToProposals maps each key to a ProposalType and returns a typed list. +// If any string is not a valid type (in this file), then return an error +func ConvertToProposals(keys []string) ([]ProposalType, error) { + valid := make(map[string]bool, len(EnableAllProposals)) + for _, key := range EnableAllProposals { + valid[string(key)] = true + } + + proposals := make([]ProposalType, len(keys)) + for i, key := range keys { + if _, ok := valid[key]; !ok { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "'%s' is not a valid ProposalType", key) + } + proposals[i] = ProposalType(key) + } + return proposals, nil +} + +func init() { // register new content types with the sdk + govtypes.RegisterProposalType(string(ProposalTypeStoreCode)) + govtypes.RegisterProposalType(string(ProposalTypeInstantiateContract)) + govtypes.RegisterProposalType(string(ProposalTypeInstantiateContract2)) + govtypes.RegisterProposalType(string(ProposalTypeMigrateContract)) + govtypes.RegisterProposalType(string(ProposalTypeSudoContract)) + govtypes.RegisterProposalType(string(ProposalTypeExecuteContract)) + govtypes.RegisterProposalType(string(ProposalTypeUpdateAdmin)) + govtypes.RegisterProposalType(string(ProposalTypeClearAdmin)) + govtypes.RegisterProposalType(string(ProposalTypePinCodes)) + govtypes.RegisterProposalType(string(ProposalTypeUnpinCodes)) + govtypes.RegisterProposalType(string(ProposalTypeUpdateInstantiateConfig)) + govtypes.RegisterProposalType(string(ProposalTypeStoreAndInstantiateContractProposal)) + govtypes.RegisterProposalTypeCodec(&StoreCodeProposal{}, "wasm/StoreCodeProposal") + govtypes.RegisterProposalTypeCodec(&InstantiateContractProposal{}, "wasm/InstantiateContractProposal") + govtypes.RegisterProposalTypeCodec(&InstantiateContract2Proposal{}, "wasm/InstantiateContract2Proposal") + govtypes.RegisterProposalTypeCodec(&MigrateContractProposal{}, "wasm/MigrateContractProposal") + govtypes.RegisterProposalTypeCodec(&SudoContractProposal{}, "wasm/SudoContractProposal") + govtypes.RegisterProposalTypeCodec(&ExecuteContractProposal{}, "wasm/ExecuteContractProposal") + govtypes.RegisterProposalTypeCodec(&UpdateAdminProposal{}, "wasm/UpdateAdminProposal") + govtypes.RegisterProposalTypeCodec(&ClearAdminProposal{}, "wasm/ClearAdminProposal") + govtypes.RegisterProposalTypeCodec(&PinCodesProposal{}, "wasm/PinCodesProposal") + govtypes.RegisterProposalTypeCodec(&UnpinCodesProposal{}, "wasm/UnpinCodesProposal") + govtypes.RegisterProposalTypeCodec(&UpdateInstantiateConfigProposal{}, "wasm/UpdateInstantiateConfigProposal") + govtypes.RegisterProposalTypeCodec(&StoreAndInstantiateContractProposal{}, "wasm/StoreAndInstantiateContractProposal") +} + +func NewStoreCodeProposal( + title string, + description string, + runAs string, + wasmBz []byte, + permission *AccessConfig, + unpinCode bool, + source string, + builder string, + codeHash []byte, +) *StoreCodeProposal { + return &StoreCodeProposal{title, description, runAs, wasmBz, permission, unpinCode, source, builder, codeHash} +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p StoreCodeProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *StoreCodeProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p StoreCodeProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p StoreCodeProposal) ProposalType() string { return string(ProposalTypeStoreCode) } + +// ValidateBasic validates the proposal +func (p StoreCodeProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.RunAs); err != nil { + return sdkerrors.Wrap(err, "run as") + } + + if err := validateWasmCode(p.WASMByteCode, MaxProposalWasmSize); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error()) + } + + if p.InstantiatePermission != nil { + if err := p.InstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + } + + if err := ValidateVerificationInfo(p.Source, p.Builder, p.CodeHash); err != nil { + return sdkerrors.Wrapf(err, "code verification info") + } + return nil +} + +// String implements the Stringer interface. +func (p StoreCodeProposal) String() string { + return fmt.Sprintf(`Store Code Proposal: + Title: %s + Description: %s + Run as: %s + WasmCode: %X + Source: %s + Builder: %s + Code Hash: %X +`, p.Title, p.Description, p.RunAs, p.WASMByteCode, p.Source, p.Builder, p.CodeHash) +} + +// MarshalYAML pretty prints the wasm byte code +func (p StoreCodeProposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + RunAs string `yaml:"run_as"` + WASMByteCode string `yaml:"wasm_byte_code"` + InstantiatePermission *AccessConfig `yaml:"instantiate_permission"` + Source string `yaml:"source"` + Builder string `yaml:"builder"` + CodeHash string `yaml:"code_hash"` + }{ + Title: p.Title, + Description: p.Description, + RunAs: p.RunAs, + WASMByteCode: base64.StdEncoding.EncodeToString(p.WASMByteCode), + InstantiatePermission: p.InstantiatePermission, + Source: p.Source, + Builder: p.Builder, + CodeHash: hex.EncodeToString(p.CodeHash), + }, nil +} + +func NewInstantiateContractProposal( + title string, + description string, + runAs string, + admin string, + codeID uint64, + label string, + msg RawContractMessage, + funds sdk.Coins, +) *InstantiateContractProposal { + return &InstantiateContractProposal{title, description, runAs, admin, codeID, label, msg, funds} +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p InstantiateContractProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *InstantiateContractProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p InstantiateContractProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p InstantiateContractProposal) ProposalType() string { + return string(ProposalTypeInstantiateContract) +} + +// ValidateBasic validates the proposal +func (p InstantiateContractProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.RunAs); err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "run as") + } + + if p.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + + if err := ValidateLabel(p.Label); err != nil { + return err + } + + if !p.Funds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(p.Admin) != 0 { + if _, err := sdk.AccAddressFromBech32(p.Admin); err != nil { + return err + } + } + if err := p.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +// String implements the Stringer interface. +func (p InstantiateContractProposal) String() string { + return fmt.Sprintf(`Instantiate Code Proposal: + Title: %s + Description: %s + Run as: %s + Admin: %s + Code id: %d + Label: %s + Msg: %q + Funds: %s +`, p.Title, p.Description, p.RunAs, p.Admin, p.CodeID, p.Label, p.Msg, p.Funds) +} + +// MarshalYAML pretty prints the init message +func (p InstantiateContractProposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + RunAs string `yaml:"run_as"` + Admin string `yaml:"admin"` + CodeID uint64 `yaml:"code_id"` + Label string `yaml:"label"` + Msg string `yaml:"msg"` + Funds sdk.Coins `yaml:"funds"` + }{ + Title: p.Title, + Description: p.Description, + RunAs: p.RunAs, + Admin: p.Admin, + CodeID: p.CodeID, + Label: p.Label, + Msg: string(p.Msg), + Funds: p.Funds, + }, nil +} + +func NewInstantiateContract2Proposal( + title string, + description string, + runAs string, + admin string, + codeID uint64, + label string, + msg RawContractMessage, + funds sdk.Coins, + salt []byte, + fixMsg bool, +) *InstantiateContract2Proposal { + return &InstantiateContract2Proposal{title, description, runAs, admin, codeID, label, msg, funds, salt, fixMsg} +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p InstantiateContract2Proposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *InstantiateContract2Proposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p InstantiateContract2Proposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p InstantiateContract2Proposal) ProposalType() string { + return string(ProposalTypeInstantiateContract2) +} + +// ValidateBasic validates the proposal +func (p InstantiateContract2Proposal) ValidateBasic() error { + // Validate title and description + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + // Validate run as + if _, err := sdk.AccAddressFromBech32(p.RunAs); err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "run as") + } + // Validate admin + if len(p.Admin) != 0 { + if _, err := sdk.AccAddressFromBech32(p.Admin); err != nil { + return err + } + } + // Validate codeid + if p.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + // Validate label + if err := ValidateLabel(p.Label); err != nil { + return err + } + // Validate msg + if err := p.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + // Validate funds + if !p.Funds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + // Validate salt + if len(p.Salt) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "salt is required") + } + return nil +} + +// String implements the Stringer interface. +func (p InstantiateContract2Proposal) String() string { + return fmt.Sprintf(`Instantiate Code Proposal: + Title: %s + Description: %s + Run as: %s + Admin: %s + Code id: %d + Label: %s + Msg: %q + Funds: %s + Salt: %X +`, p.Title, p.Description, p.RunAs, p.Admin, p.CodeID, p.Label, p.Msg, p.Funds, p.Salt) +} + +// MarshalYAML pretty prints the init message +func (p InstantiateContract2Proposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + RunAs string `yaml:"run_as"` + Admin string `yaml:"admin"` + CodeID uint64 `yaml:"code_id"` + Label string `yaml:"label"` + Msg string `yaml:"msg"` + Funds sdk.Coins `yaml:"funds"` + Salt string `yaml:"salt"` + }{ + Title: p.Title, + Description: p.Description, + RunAs: p.RunAs, + Admin: p.Admin, + CodeID: p.CodeID, + Label: p.Label, + Msg: string(p.Msg), + Funds: p.Funds, + Salt: base64.StdEncoding.EncodeToString(p.Salt), + }, nil +} + +func NewStoreAndInstantiateContractProposal( + title string, + description string, + runAs string, + wasmBz []byte, + source string, + builder string, + codeHash []byte, + permission *AccessConfig, + unpinCode bool, + admin string, + label string, + msg RawContractMessage, + funds sdk.Coins, +) *StoreAndInstantiateContractProposal { + return &StoreAndInstantiateContractProposal{ + Title: title, + Description: description, + RunAs: runAs, + WASMByteCode: wasmBz, + Source: source, + Builder: builder, + CodeHash: codeHash, + InstantiatePermission: permission, + UnpinCode: unpinCode, + Admin: admin, + Label: label, + Msg: msg, + Funds: funds, + } +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p StoreAndInstantiateContractProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *StoreAndInstantiateContractProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p StoreAndInstantiateContractProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p StoreAndInstantiateContractProposal) ProposalType() string { + return string(ProposalTypeStoreAndInstantiateContractProposal) +} + +// ValidateBasic validates the proposal +func (p StoreAndInstantiateContractProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.RunAs); err != nil { + return sdkerrors.Wrap(err, "run as") + } + + if err := validateWasmCode(p.WASMByteCode, MaxProposalWasmSize); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error()) + } + + if err := ValidateVerificationInfo(p.Source, p.Builder, p.CodeHash); err != nil { + return sdkerrors.Wrap(err, "code info") + } + + if p.InstantiatePermission != nil { + if err := p.InstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + } + + if err := ValidateLabel(p.Label); err != nil { + return err + } + + if !p.Funds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(p.Admin) != 0 { + if _, err := sdk.AccAddressFromBech32(p.Admin); err != nil { + return err + } + } + if err := p.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +// String implements the Stringer interface. +func (p StoreAndInstantiateContractProposal) String() string { + return fmt.Sprintf(`Store And Instantiate Coontract Proposal: + Title: %s + Description: %s + Run as: %s + WasmCode: %X + Source: %s + Builder: %s + Code Hash: %X + Instantiate permission: %s + Unpin code: %t + Admin: %s + Label: %s + Msg: %q + Funds: %s +`, p.Title, p.Description, p.RunAs, p.WASMByteCode, p.Source, p.Builder, p.CodeHash, p.InstantiatePermission, p.UnpinCode, p.Admin, p.Label, p.Msg, p.Funds) +} + +// MarshalYAML pretty prints the wasm byte code and the init message +func (p StoreAndInstantiateContractProposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + RunAs string `yaml:"run_as"` + WASMByteCode string `yaml:"wasm_byte_code"` + Source string `yaml:"source"` + Builder string `yaml:"builder"` + CodeHash string `yaml:"code_hash"` + InstantiatePermission *AccessConfig `yaml:"instantiate_permission"` + UnpinCode bool `yaml:"unpin_code"` + Admin string `yaml:"admin"` + Label string `yaml:"label"` + Msg string `yaml:"msg"` + Funds sdk.Coins `yaml:"funds"` + }{ + Title: p.Title, + Description: p.Description, + RunAs: p.RunAs, + WASMByteCode: base64.StdEncoding.EncodeToString(p.WASMByteCode), + InstantiatePermission: p.InstantiatePermission, + UnpinCode: p.UnpinCode, + Admin: p.Admin, + Label: p.Label, + Source: p.Source, + Builder: p.Builder, + CodeHash: hex.EncodeToString(p.CodeHash), + Msg: string(p.Msg), + Funds: p.Funds, + }, nil +} + +func NewMigrateContractProposal( + title string, + description string, + contract string, + codeID uint64, + msg RawContractMessage, +) *MigrateContractProposal { + return &MigrateContractProposal{ + Title: title, + Description: description, + Contract: contract, + CodeID: codeID, + Msg: msg, + } +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p MigrateContractProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *MigrateContractProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p MigrateContractProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p MigrateContractProposal) ProposalType() string { return string(ProposalTypeMigrateContract) } + +// ValidateBasic validates the proposal +func (p MigrateContractProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if p.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required") + } + if _, err := sdk.AccAddressFromBech32(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := p.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +// String implements the Stringer interface. +func (p MigrateContractProposal) String() string { + return fmt.Sprintf(`Migrate Contract Proposal: + Title: %s + Description: %s + Contract: %s + Code id: %d + Msg: %q +`, p.Title, p.Description, p.Contract, p.CodeID, p.Msg) +} + +// MarshalYAML pretty prints the migrate message +func (p MigrateContractProposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Contract string `yaml:"contract"` + CodeID uint64 `yaml:"code_id"` + Msg string `yaml:"msg"` + }{ + Title: p.Title, + Description: p.Description, + Contract: p.Contract, + CodeID: p.CodeID, + Msg: string(p.Msg), + }, nil +} + +func NewSudoContractProposal( + title string, + description string, + contract string, + msg RawContractMessage, +) *SudoContractProposal { + return &SudoContractProposal{ + Title: title, + Description: description, + Contract: contract, + Msg: msg, + } +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p SudoContractProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *SudoContractProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p SudoContractProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p SudoContractProposal) ProposalType() string { return string(ProposalTypeSudoContract) } + +// ValidateBasic validates the proposal +func (p SudoContractProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := p.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +// String implements the Stringer interface. +func (p SudoContractProposal) String() string { + return fmt.Sprintf(`Migrate Contract Proposal: + Title: %s + Description: %s + Contract: %s + Msg: %q +`, p.Title, p.Description, p.Contract, p.Msg) +} + +// MarshalYAML pretty prints the migrate message +func (p SudoContractProposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Contract string `yaml:"contract"` + Msg string `yaml:"msg"` + }{ + Title: p.Title, + Description: p.Description, + Contract: p.Contract, + Msg: string(p.Msg), + }, nil +} + +func NewExecuteContractProposal( + title string, + description string, + runAs string, + contract string, + msg RawContractMessage, + funds sdk.Coins, +) *ExecuteContractProposal { + return &ExecuteContractProposal{title, description, runAs, contract, msg, funds} +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p ExecuteContractProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *ExecuteContractProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p ExecuteContractProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p ExecuteContractProposal) ProposalType() string { return string(ProposalTypeExecuteContract) } + +// ValidateBasic validates the proposal +func (p ExecuteContractProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if _, err := sdk.AccAddressFromBech32(p.RunAs); err != nil { + return sdkerrors.Wrap(err, "run as") + } + if !p.Funds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + if err := p.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +// String implements the Stringer interface. +func (p ExecuteContractProposal) String() string { + return fmt.Sprintf(`Migrate Contract Proposal: + Title: %s + Description: %s + Contract: %s + Run as: %s + Msg: %q + Funds: %s +`, p.Title, p.Description, p.Contract, p.RunAs, p.Msg, p.Funds) +} + +// MarshalYAML pretty prints the migrate message +func (p ExecuteContractProposal) MarshalYAML() (interface{}, error) { + return struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Contract string `yaml:"contract"` + Msg string `yaml:"msg"` + RunAs string `yaml:"run_as"` + Funds sdk.Coins `yaml:"funds"` + }{ + Title: p.Title, + Description: p.Description, + Contract: p.Contract, + Msg: string(p.Msg), + RunAs: p.RunAs, + Funds: p.Funds, + }, nil +} + +func NewUpdateAdminProposal( + title string, + description string, + newAdmin string, + contract string, +) *UpdateAdminProposal { + return &UpdateAdminProposal{title, description, newAdmin, contract} +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p UpdateAdminProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *UpdateAdminProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p UpdateAdminProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p UpdateAdminProposal) ProposalType() string { return string(ProposalTypeUpdateAdmin) } + +// ValidateBasic validates the proposal +func (p UpdateAdminProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if _, err := sdk.AccAddressFromBech32(p.NewAdmin); err != nil { + return sdkerrors.Wrap(err, "new admin") + } + return nil +} + +// String implements the Stringer interface. +func (p UpdateAdminProposal) String() string { + return fmt.Sprintf(`Update Contract Admin Proposal: + Title: %s + Description: %s + Contract: %s + New Admin: %s +`, p.Title, p.Description, p.Contract, p.NewAdmin) +} + +func NewClearAdminProposal( + title string, + description string, + contract string, +) *ClearAdminProposal { + return &ClearAdminProposal{title, description, contract} +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p ClearAdminProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *ClearAdminProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p ClearAdminProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p ClearAdminProposal) ProposalType() string { return string(ProposalTypeClearAdmin) } + +// ValidateBasic validates the proposal +func (p ClearAdminProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if _, err := sdk.AccAddressFromBech32(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + return nil +} + +// String implements the Stringer interface. +func (p ClearAdminProposal) String() string { + return fmt.Sprintf(`Clear Contract Admin Proposal: + Title: %s + Description: %s + Contract: %s +`, p.Title, p.Description, p.Contract) +} + +func NewPinCodesProposal( + title string, + description string, + codeIDs []uint64, +) *PinCodesProposal { + return &PinCodesProposal{ + Title: title, + Description: description, + CodeIDs: codeIDs, + } +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p PinCodesProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *PinCodesProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p PinCodesProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p PinCodesProposal) ProposalType() string { return string(ProposalTypePinCodes) } + +// ValidateBasic validates the proposal +func (p PinCodesProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if len(p.CodeIDs) == 0 { + return sdkerrors.Wrap(ErrEmpty, "code ids") + } + return nil +} + +// String implements the Stringer interface. +func (p PinCodesProposal) String() string { + return fmt.Sprintf(`Pin Wasm Codes Proposal: + Title: %s + Description: %s + Codes: %v +`, p.Title, p.Description, p.CodeIDs) +} + +func NewUnpinCodesProposal( + title string, + description string, + codeIDs []uint64, +) *UnpinCodesProposal { + return &UnpinCodesProposal{ + Title: title, + Description: description, + CodeIDs: codeIDs, + } +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p UnpinCodesProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *UnpinCodesProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p UnpinCodesProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p UnpinCodesProposal) ProposalType() string { return string(ProposalTypeUnpinCodes) } + +// ValidateBasic validates the proposal +func (p UnpinCodesProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if len(p.CodeIDs) == 0 { + return sdkerrors.Wrap(ErrEmpty, "code ids") + } + return nil +} + +// String implements the Stringer interface. +func (p UnpinCodesProposal) String() string { + return fmt.Sprintf(`Unpin Wasm Codes Proposal: + Title: %s + Description: %s + Codes: %v +`, p.Title, p.Description, p.CodeIDs) +} + +func validateProposalCommons(title, description string) error { + if strings.TrimSpace(title) != title { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title must not start/end with white spaces") + } + if len(title) == 0 { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title cannot be blank") + } + if len(title) > govtypes.MaxTitleLength { + return sdkerrors.Wrapf(govtypes.ErrInvalidProposalContent, "proposal title is longer than max length of %d", govtypes.MaxTitleLength) + } + if strings.TrimSpace(description) != description { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description must not start/end with white spaces") + } + if len(description) == 0 { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description cannot be blank") + } + if len(description) > govtypes.MaxDescriptionLength { + return sdkerrors.Wrapf(govtypes.ErrInvalidProposalContent, "proposal description is longer than max length of %d", govtypes.MaxDescriptionLength) + } + return nil +} + +func NewUpdateInstantiateConfigProposal( + title string, + description string, + accessConfigUpdates ...AccessConfigUpdate, +) *UpdateInstantiateConfigProposal { + return &UpdateInstantiateConfigProposal{ + Title: title, + Description: description, + AccessConfigUpdates: accessConfigUpdates, + } +} + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p UpdateInstantiateConfigProposal) ProposalRoute() string { return RouterKey } + +// GetTitle returns the title of the proposal +func (p *UpdateInstantiateConfigProposal) GetTitle() string { return p.Title } + +// GetDescription returns the human readable description of the proposal +func (p UpdateInstantiateConfigProposal) GetDescription() string { return p.Description } + +// ProposalType returns the type +func (p UpdateInstantiateConfigProposal) ProposalType() string { + return string(ProposalTypeUpdateInstantiateConfig) +} + +// ValidateBasic validates the proposal +func (p UpdateInstantiateConfigProposal) ValidateBasic() error { + if err := validateProposalCommons(p.Title, p.Description); err != nil { + return err + } + if len(p.AccessConfigUpdates) == 0 { + return sdkerrors.Wrap(ErrEmpty, "code updates") + } + dedup := make(map[uint64]bool) + for _, codeUpdate := range p.AccessConfigUpdates { + _, found := dedup[codeUpdate.CodeID] + if found { + return sdkerrors.Wrapf(ErrDuplicate, "duplicate code: %d", codeUpdate.CodeID) + } + if err := codeUpdate.InstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + dedup[codeUpdate.CodeID] = true + } + return nil +} + +// String implements the Stringer interface. +func (p UpdateInstantiateConfigProposal) String() string { + return fmt.Sprintf(`Update Instantiate Config Proposal: + Title: %s + Description: %s + AccessConfigUpdates: %v +`, p.Title, p.Description, p.AccessConfigUpdates) +} + +// String implements the Stringer interface. +func (c AccessConfigUpdate) String() string { + return fmt.Sprintf(`AccessConfigUpdate: + CodeID: %d + AccessConfig: %v +`, c.CodeID, c.InstantiatePermission) +} diff --git a/x/wasm/types/proposal_test.go b/x/wasm/types/proposal_test.go new file mode 100644 index 00000000..a558744f --- /dev/null +++ b/x/wasm/types/proposal_test.go @@ -0,0 +1,1141 @@ +package types + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestValidateProposalCommons(t *testing.T) { + type commonProposal struct { + Title, Description string + } + + specs := map[string]struct { + src commonProposal + expErr bool + }{ + "all good": {src: commonProposal{ + Title: "Foo", + Description: "Bar", + }}, + "prevent empty title": { + src: commonProposal{ + Description: "Bar", + }, + expErr: true, + }, + "prevent white space only title": { + src: commonProposal{ + Title: " ", + Description: "Bar", + }, + expErr: true, + }, + "prevent leading white spaces in title": { + src: commonProposal{ + Title: " Foo", + Description: "Bar", + }, + expErr: true, + }, + "prevent title exceeds max length ": { + src: commonProposal{ + Title: strings.Repeat("a", govtypes.MaxTitleLength+1), + Description: "Bar", + }, + expErr: true, + }, + "prevent empty description": { + src: commonProposal{ + Title: "Foo", + }, + expErr: true, + }, + "prevent leading white spaces in description": { + src: commonProposal{ + Title: "Foo", + Description: " Bar", + }, + expErr: true, + }, + "prevent white space only description": { + src: commonProposal{ + Title: "Foo", + Description: " ", + }, + expErr: true, + }, + "prevent descr exceeds max length ": { + src: commonProposal{ + Title: "Foo", + Description: strings.Repeat("a", govtypes.MaxDescriptionLength+1), + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := validateProposalCommons(spec.src.Title, spec.src.Description) + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateStoreCodeProposal(t *testing.T) { + var ( + anyAddress sdk.AccAddress = bytes.Repeat([]byte{0x0}, ContractAddrLen) + invalidAddress = "invalid address" + ) + + specs := map[string]struct { + src *StoreCodeProposal + expErr bool + }{ + "all good": { + src: StoreCodeProposalFixture(), + }, + "all good no code verification info": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Source = "" + p.Builder = "" + p.CodeHash = nil + }), + }, + "source missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Source = "" + }), + expErr: true, + }, + "builder missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Builder = "" + }), + expErr: true, + }, + "code hash missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.CodeHash = nil + }), + expErr: true, + }, + "with instantiate permission": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + accessConfig := AccessTypeOnlyAddress.With(anyAddress) + p.InstantiatePermission = &accessConfig + }), + }, + "base data missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Title = "" + }), + expErr: true, + }, + "run_as missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.RunAs = "" + }), + expErr: true, + }, + "run_as invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + "wasm code missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = nil + }), + expErr: true, + }, + "wasm code invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = bytes.Repeat([]byte{0x0}, MaxProposalWasmSize+1) + }), + expErr: true, + }, + "with invalid instantiate permission": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.InstantiatePermission = &AccessConfig{} + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateInstantiateContractProposal(t *testing.T) { + invalidAddress := "invalid address" + + specs := map[string]struct { + src *InstantiateContractProposal + expErr bool + }{ + "all good": { + src: InstantiateContractProposalFixture(), + }, + "without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Admin = "" + }), + }, + "without init msg": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Msg = nil + }), + expErr: true, + }, + "with invalid init msg": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Msg = []byte("not a json string") + }), + expErr: true, + }, + "without init funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Funds = nil + }), + }, + "base data missing": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Title = "" + }), + expErr: true, + }, + "run_as missing": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.RunAs = "" + }), + expErr: true, + }, + "run_as invalid": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + "admin invalid": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Admin = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.CodeID = 0 + }), + expErr: true, + }, + "label empty": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Label = "" + }), + expErr: true, + }, + "init funds negative": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(-1)}} + }), + expErr: true, + }, + "init funds with duplicates": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "foo", Amount: sdk.NewInt(2)}} + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateInstantiateContract2Proposal(t *testing.T) { + invalidAddress := "invalid address" + + specs := map[string]struct { + src *InstantiateContract2Proposal + expErr bool + }{ + "all good": { + src: InstantiateContract2ProposalFixture(), + }, + "without admin": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Admin = "" + }), + }, + "without init msg": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Msg = nil + }), + expErr: true, + }, + "with invalid init msg": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Msg = []byte("not a json string") + }), + expErr: true, + }, + "without init funds": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Funds = nil + }), + }, + "base data missing": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Title = "" + }), + expErr: true, + }, + "run_as missing": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.RunAs = "" + }), + expErr: true, + }, + "run_as invalid": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + "admin invalid": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Admin = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.CodeID = 0 + }), + expErr: true, + }, + "label empty": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Label = "" + }), + expErr: true, + }, + "init funds negative": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(-1)}} + }), + expErr: true, + }, + "init funds with duplicates": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "foo", Amount: sdk.NewInt(2)}} + }), + expErr: true, + }, + "init with empty salt": { + src: InstantiateContract2ProposalFixture(func(p *InstantiateContract2Proposal) { + p.Salt = nil + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateStoreAndInstantiateContractProposal(t *testing.T) { + var ( + anyAddress sdk.AccAddress = bytes.Repeat([]byte{0x0}, ContractAddrLen) + invalidAddress = "invalid address" + ) + + specs := map[string]struct { + src *StoreAndInstantiateContractProposal + expErr bool + }{ + "all good": { + src: StoreAndInstantiateContractProposalFixture(), + }, + "all good no code verification info": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Source = "" + p.Builder = "" + p.CodeHash = nil + }), + }, + "source missing": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Source = "" + }), + expErr: true, + }, + "builder missing": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Builder = "" + }), + expErr: true, + }, + "code hash missing": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.CodeHash = nil + }), + expErr: true, + }, + "with instantiate permission": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + accessConfig := AccessTypeOnlyAddress.With(anyAddress) + p.InstantiatePermission = &accessConfig + }), + }, + "base data missing": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Title = "" + }), + expErr: true, + }, + "run_as missing": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.RunAs = "" + }), + expErr: true, + }, + "run_as invalid": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + "wasm code missing": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.WASMByteCode = nil + }), + expErr: true, + }, + "wasm code invalid": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.WASMByteCode = bytes.Repeat([]byte{0x0}, MaxProposalWasmSize+1) + }), + expErr: true, + }, + "with invalid instantiate permission": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.InstantiatePermission = &AccessConfig{} + }), + expErr: true, + }, + "without admin": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Admin = "" + }), + }, + "without init msg": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Msg = nil + }), + expErr: true, + }, + "with invalid init msg": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Msg = []byte("not a json string") + }), + expErr: true, + }, + "without init funds": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Funds = nil + }), + }, + "admin invalid": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Admin = invalidAddress + }), + expErr: true, + }, + "label empty": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Label = "" + }), + expErr: true, + }, + "init funds negative": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(-1)}} + }), + expErr: true, + }, + "init funds with duplicates": { + src: StoreAndInstantiateContractProposalFixture(func(p *StoreAndInstantiateContractProposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "foo", Amount: sdk.NewInt(2)}} + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateMigrateContractProposal(t *testing.T) { + invalidAddress := "invalid address2" + + specs := map[string]struct { + src *MigrateContractProposal + expErr bool + }{ + "all good": { + src: MigrateContractProposalFixture(), + }, + "without migrate msg": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Msg = nil + }), + expErr: true, + }, + "migrate msg with invalid json": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Msg = []byte("not a json message") + }), + expErr: true, + }, + "base data missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Title = "" + }), + expErr: true, + }, + "contract missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Contract = "" + }), + expErr: true, + }, + "contract invalid": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.CodeID = 0 + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateSudoContractProposal(t *testing.T) { + invalidAddress := "invalid address" + + specs := map[string]struct { + src *SudoContractProposal + expErr bool + }{ + "all good": { + src: SudoContractProposalFixture(), + }, + "msg is nil": { + src: SudoContractProposalFixture(func(p *SudoContractProposal) { + p.Msg = nil + }), + expErr: true, + }, + "msg with invalid json": { + src: SudoContractProposalFixture(func(p *SudoContractProposal) { + p.Msg = []byte("not a json message") + }), + expErr: true, + }, + "base data missing": { + src: SudoContractProposalFixture(func(p *SudoContractProposal) { + p.Title = "" + }), + expErr: true, + }, + "contract missing": { + src: SudoContractProposalFixture(func(p *SudoContractProposal) { + p.Contract = "" + }), + expErr: true, + }, + "contract invalid": { + src: SudoContractProposalFixture(func(p *SudoContractProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateExecuteContractProposal(t *testing.T) { + invalidAddress := "invalid address" + + specs := map[string]struct { + src *ExecuteContractProposal + expErr bool + }{ + "all good": { + src: ExecuteContractProposalFixture(), + }, + "msg is nil": { + src: ExecuteContractProposalFixture(func(p *ExecuteContractProposal) { + p.Msg = nil + }), + expErr: true, + }, + "msg with invalid json": { + src: ExecuteContractProposalFixture(func(p *ExecuteContractProposal) { + p.Msg = []byte("not a valid json message") + }), + expErr: true, + }, + "base data missing": { + src: ExecuteContractProposalFixture(func(p *ExecuteContractProposal) { + p.Title = "" + }), + expErr: true, + }, + "contract missing": { + src: ExecuteContractProposalFixture(func(p *ExecuteContractProposal) { + p.Contract = "" + }), + expErr: true, + }, + "contract invalid": { + src: ExecuteContractProposalFixture(func(p *ExecuteContractProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "run as is invalid": { + src: ExecuteContractProposalFixture(func(p *ExecuteContractProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateUpdateAdminProposal(t *testing.T) { + invalidAddress := "invalid address" + + specs := map[string]struct { + src *UpdateAdminProposal + expErr bool + }{ + "all good": { + src: UpdateAdminProposalFixture(), + }, + "base data missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Title = "" + }), + expErr: true, + }, + "contract missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Contract = "" + }), + expErr: true, + }, + "contract invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "admin missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.NewAdmin = "" + }), + expErr: true, + }, + "admin invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.NewAdmin = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateClearAdminProposal(t *testing.T) { + invalidAddress := "invalid address" + + specs := map[string]struct { + src *ClearAdminProposal + expErr bool + }{ + "all good": { + src: ClearAdminProposalFixture(), + }, + "base data missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Title = "" + }), + expErr: true, + }, + "contract missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Contract = "" + }), + expErr: true, + }, + "contract invalid": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestProposalStrings(t *testing.T) { + specs := map[string]struct { + src govtypes.Content + exp string + }{ + "store code": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = []byte{0o1, 0o2, 0o3, 0o4, 0o5, 0o6, 0o7, 0x08, 0x09, 0x0a} + }), + exp: `Store Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 + WasmCode: 0102030405060708090A + Source: https://example.com/ + Builder: cosmwasm/workspace-optimizer:v0.12.8 + Code Hash: 6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D +`, + }, + "instantiate contract": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "bar", Amount: sdk.NewInt(2)}} + }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 + Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 + Code id: 1 + Label: testing + Msg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\"}" + Funds: 1foo,2bar +`, + }, + "instantiate contract without funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Funds = nil }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 + Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 + Code id: 1 + Label: testing + Msg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\"}" + Funds: +`, + }, + "instantiate contract without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Admin = "" }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 + Admin: + Code id: 1 + Label: testing + Msg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\"}" + Funds: +`, + }, + "migrate contract": { + src: MigrateContractProposalFixture(), + exp: `Migrate Contract Proposal: + Title: Foo + Description: Bar + Contract: cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr + Code id: 1 + Msg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4\"}" +`, + }, + "update admin": { + src: UpdateAdminProposalFixture(), + exp: `Update Contract Admin Proposal: + Title: Foo + Description: Bar + Contract: cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr + New Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +`, + }, + "clear admin": { + src: ClearAdminProposalFixture(), + exp: `Clear Contract Admin Proposal: + Title: Foo + Description: Bar + Contract: cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr +`, + }, + "pin codes": { + src: &PinCodesProposal{ + Title: "Foo", + Description: "Bar", + CodeIDs: []uint64{1, 2, 3}, + }, + exp: `Pin Wasm Codes Proposal: + Title: Foo + Description: Bar + Codes: [1 2 3] +`, + }, + "unpin codes": { + src: &UnpinCodesProposal{ + Title: "Foo", + Description: "Bar", + CodeIDs: []uint64{3, 2, 1}, + }, + exp: `Unpin Wasm Codes Proposal: + Title: Foo + Description: Bar + Codes: [3 2 1] +`, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + assert.Equal(t, spec.exp, spec.src.String()) + }) + } +} + +func TestProposalYaml(t *testing.T) { + specs := map[string]struct { + src govtypes.Content + exp string + }{ + "store code": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = []byte{0o1, 0o2, 0o3, 0o4, 0o5, 0o6, 0o7, 0x08, 0x09, 0x0a} + }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +wasm_byte_code: AQIDBAUGBwgJCg== +instantiate_permission: null +source: https://example.com/ +builder: cosmwasm/workspace-optimizer:v0.12.8 +code_hash: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d +`, + }, + "instantiate contract": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Funds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "bar", Amount: sdk.NewInt(2)}} + }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +code_id: 1 +label: testing +msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4","beneficiary":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4"}' +funds: +- denom: foo + amount: "1" +- denom: bar + amount: "2" +`, + }, + "instantiate contract without funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Funds = nil }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +code_id: 1 +label: testing +msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4","beneficiary":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4"}' +funds: [] +`, + }, + "instantiate contract without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Admin = "" }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +admin: "" +code_id: 1 +label: testing +msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4","beneficiary":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4"}' +funds: [] +`, + }, + "migrate contract": { + src: MigrateContractProposalFixture(), + exp: `title: Foo +description: Bar +contract: cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr +code_id: 1 +msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4"}' +`, + }, + "update admin": { + src: UpdateAdminProposalFixture(), + exp: `title: Foo +description: Bar +new_admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4 +contract: cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr +`, + }, + "clear admin": { + src: ClearAdminProposalFixture(), + exp: `title: Foo +description: Bar +contract: cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr +`, + }, + "pin codes": { + src: &PinCodesProposal{ + Title: "Foo", + Description: "Bar", + CodeIDs: []uint64{1, 2, 3}, + }, + exp: `title: Foo +description: Bar +code_ids: +- 1 +- 2 +- 3 +`, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + v, err := yaml.Marshal(&spec.src) + require.NoError(t, err) + assert.Equal(t, spec.exp, string(v)) + }) + } +} + +func TestConvertToProposals(t *testing.T) { + cases := map[string]struct { + input string + isError bool + proposals []ProposalType + }{ + "one proper item": { + input: "UpdateAdmin", + proposals: []ProposalType{ProposalTypeUpdateAdmin}, + }, + "multiple proper items": { + input: "StoreCode,InstantiateContract,MigrateContract", + proposals: []ProposalType{ProposalTypeStoreCode, ProposalTypeInstantiateContract, ProposalTypeMigrateContract}, + }, + "empty trailing item": { + input: "StoreCode,", + isError: true, + }, + "invalid item": { + input: "StoreCode,InvalidProposalType", + isError: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + chunks := strings.Split(tc.input, ",") + proposals, err := ConvertToProposals(chunks) + if tc.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, proposals, tc.proposals) + } + }) + } +} + +func TestUnmarshalContentFromJson(t *testing.T) { + specs := map[string]struct { + src string + got govtypes.Content + exp govtypes.Content + }{ + "instantiate ": { + src: ` +{ + "title": "foo", + "description": "bar", + "admin": "myAdminAddress", + "code_id": 1, + "funds": [{"denom": "ALX", "amount": "2"},{"denom": "BLX","amount": "3"}], + "msg": {}, + "label": "testing", + "run_as": "myRunAsAddress" +}`, + got: &InstantiateContractProposal{}, + exp: &InstantiateContractProposal{ + Title: "foo", + Description: "bar", + RunAs: "myRunAsAddress", + Admin: "myAdminAddress", + CodeID: 1, + Label: "testing", + Msg: []byte("{}"), + Funds: sdk.NewCoins(sdk.NewCoin("ALX", sdk.NewInt(2)), sdk.NewCoin("BLX", sdk.NewInt(3))), + }, + }, + "migrate ": { + src: ` +{ + "title": "foo", + "description": "bar", + "code_id": 1, + "contract": "myContractAddr", + "msg": {}, + "run_as": "myRunAsAddress" +}`, + got: &MigrateContractProposal{}, + exp: &MigrateContractProposal{ + Title: "foo", + Description: "bar", + Contract: "myContractAddr", + CodeID: 1, + Msg: []byte("{}"), + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + require.NoError(t, json.Unmarshal([]byte(spec.src), spec.got)) + assert.Equal(t, spec.exp, spec.got) + }) + } +} + +func TestProposalJsonSignBytes(t *testing.T) { + const myInnerMsg = `{"foo":"bar"}` + specs := map[string]struct { + src govtypes.Content + exp string + }{ + "instantiate contract": { + src: &InstantiateContractProposal{Msg: RawContractMessage(myInnerMsg)}, + exp: ` +{ + "type":"cosmos-sdk/MsgSubmitProposal", + "value":{"content":{"type":"wasm/InstantiateContractProposal","value":{"funds":[],"msg":{"foo":"bar"}}},"initial_deposit":[]} +}`, + }, + "migrate contract": { + src: &MigrateContractProposal{Msg: RawContractMessage(myInnerMsg)}, + exp: ` +{ + "type":"cosmos-sdk/MsgSubmitProposal", + "value":{"content":{"type":"wasm/MigrateContractProposal","value":{"msg":{"foo":"bar"}}},"initial_deposit":[]} +}`, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + msg, err := govtypes.NewMsgSubmitProposal(spec.src, sdk.NewCoins(), []byte{}) + require.NoError(t, err) + + bz := msg.GetSignBytes() + assert.JSONEq(t, spec.exp, string(bz), "raw: %s", string(bz)) + }) + } +} diff --git a/x/wasm/types/test_fixtures.go b/x/wasm/types/test_fixtures.go new file mode 100644 index 00000000..e84b61fd --- /dev/null +++ b/x/wasm/types/test_fixtures.go @@ -0,0 +1,443 @@ +package types + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func GenesisFixture(mutators ...func(*GenesisState)) GenesisState { + const ( + numCodes = 2 + numContracts = 2 + numSequences = 2 + numMsg = 3 + ) + + fixture := GenesisState{ + Params: DefaultParams(), + Codes: make([]Code, numCodes), + Contracts: make([]Contract, numContracts), + Sequences: make([]Sequence, numSequences), + } + for i := 0; i < numCodes; i++ { + fixture.Codes[i] = CodeFixture() + } + for i := 0; i < numContracts; i++ { + fixture.Contracts[i] = ContractFixture() + } + for i := 0; i < numSequences; i++ { + fixture.Sequences[i] = Sequence{ + IDKey: randBytes(5), + Value: uint64(i), + } + } + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func randBytes(n int) []byte { + r := make([]byte, n) + rand.Read(r) + return r +} + +func CodeFixture(mutators ...func(*Code)) Code { + wasmCode := randBytes(100) + + fixture := Code{ + CodeID: 1, + CodeInfo: CodeInfoFixture(WithSHA256CodeHash(wasmCode)), + CodeBytes: wasmCode, + } + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func CodeInfoFixture(mutators ...func(*CodeInfo)) CodeInfo { + wasmCode := bytes.Repeat([]byte{0x1}, 10) + codeHash := sha256.Sum256(wasmCode) + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + fixture := CodeInfo{ + CodeHash: codeHash[:], + Creator: anyAddress, + InstantiateConfig: AllowEverybody, + } + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func ContractFixture(mutators ...func(*Contract)) Contract { + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + + fixture := Contract{ + ContractAddress: anyAddress, + ContractInfo: ContractInfoFixture(RandCreatedFields), + ContractState: []Model{{Key: []byte("anyKey"), Value: []byte("anyValue")}}, + } + fixture.ContractCodeHistory = []ContractCodeHistoryEntry{ContractCodeHistoryEntryFixture(func(e *ContractCodeHistoryEntry) { + e.Updated = fixture.ContractInfo.Created + })} + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func OnlyGenesisFields(info *ContractInfo) { + info.Created = nil +} + +func RandCreatedFields(info *ContractInfo) { + info.Created = &AbsoluteTxPosition{BlockHeight: rand.Uint64(), TxIndex: rand.Uint64()} +} + +func ContractInfoFixture(mutators ...func(*ContractInfo)) ContractInfo { + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + + fixture := ContractInfo{ + CodeID: 1, + Creator: anyAddress, + Label: "any", + Created: &AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1}, + } + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +// ContractCodeHistoryEntryFixture test fixture +func ContractCodeHistoryEntryFixture(mutators ...func(*ContractCodeHistoryEntry)) ContractCodeHistoryEntry { + fixture := ContractCodeHistoryEntry{ + Operation: ContractCodeHistoryOperationTypeInit, + CodeID: 1, + Updated: ContractInfoFixture().Created, + Msg: []byte(`{"foo":"bar"}`), + } + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func WithSHA256CodeHash(wasmCode []byte) func(info *CodeInfo) { + return func(info *CodeInfo) { + codeHash := sha256.Sum256(wasmCode) + info.CodeHash = codeHash[:] + } +} + +func MsgStoreCodeFixture(mutators ...func(*MsgStoreCode)) *MsgStoreCode { + wasmIdent := []byte("\x00\x61\x73\x6D") + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + r := &MsgStoreCode{ + Sender: anyAddress, + WASMByteCode: wasmIdent, + InstantiatePermission: &AllowEverybody, + } + for _, m := range mutators { + m(r) + } + return r +} + +func MsgInstantiateContractFixture(mutators ...func(*MsgInstantiateContract)) *MsgInstantiateContract { + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + r := &MsgInstantiateContract{ + Sender: anyAddress, + Admin: anyAddress, + CodeID: 1, + Label: "testing", + Msg: []byte(`{"foo":"bar"}`), + Funds: sdk.Coins{{ + Denom: "stake", + Amount: sdk.NewInt(1), + }}, + } + for _, m := range mutators { + m(r) + } + return r +} + +func MsgExecuteContractFixture(mutators ...func(*MsgExecuteContract)) *MsgExecuteContract { + const ( + anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + firstContractAddress = "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + ) + r := &MsgExecuteContract{ + Sender: anyAddress, + Contract: firstContractAddress, + Msg: []byte(`{"do":"something"}`), + Funds: sdk.Coins{{ + Denom: "stake", + Amount: sdk.NewInt(1), + }}, + } + for _, m := range mutators { + m(r) + } + return r +} + +func StoreCodeProposalFixture(mutators ...func(*StoreCodeProposal)) *StoreCodeProposal { + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + wasm := []byte{0x0} + // got the value from shell sha256sum + codeHash, err := hex.DecodeString("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D") + if err != nil { + panic(err) + } + + p := &StoreCodeProposal{ + Title: "Foo", + Description: "Bar", + RunAs: anyAddress, + WASMByteCode: wasm, + Source: "https://example.com/", + Builder: "cosmwasm/workspace-optimizer:v0.12.8", + CodeHash: codeHash, + } + for _, m := range mutators { + m(p) + } + return p +} + +func InstantiateContractProposalFixture(mutators ...func(p *InstantiateContractProposal)) *InstantiateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, ContractAddrLen) + + initMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` + }{ + Verifier: anyValidAddress, + Beneficiary: anyValidAddress, + } + ) + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + + initMsgBz, err := json.Marshal(initMsg) + if err != nil { + panic(err) + } + p := &InstantiateContractProposal{ + Title: "Foo", + Description: "Bar", + RunAs: anyAddress, + Admin: anyAddress, + CodeID: 1, + Label: "testing", + Msg: initMsgBz, + Funds: nil, + } + + for _, m := range mutators { + m(p) + } + return p +} + +func InstantiateContract2ProposalFixture(mutators ...func(p *InstantiateContract2Proposal)) *InstantiateContract2Proposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, ContractAddrLen) + + initMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` + }{ + Verifier: anyValidAddress, + Beneficiary: anyValidAddress, + } + ) + const ( + anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + mySalt = "myDefaultSalt" + ) + + initMsgBz, err := json.Marshal(initMsg) + if err != nil { + panic(err) + } + p := &InstantiateContract2Proposal{ + Title: "Foo", + Description: "Bar", + RunAs: anyAddress, + Admin: anyAddress, + CodeID: 1, + Label: "testing", + Msg: initMsgBz, + Funds: nil, + Salt: []byte(mySalt), + FixMsg: false, + } + + for _, m := range mutators { + m(p) + } + return p +} + +func StoreAndInstantiateContractProposalFixture(mutators ...func(p *StoreAndInstantiateContractProposal)) *StoreAndInstantiateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, ContractAddrLen) + + initMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` + }{ + Verifier: anyValidAddress, + Beneficiary: anyValidAddress, + } + ) + const anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + wasm := []byte{0x0} + // got the value from shell sha256sum + codeHash, err := hex.DecodeString("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D") + if err != nil { + panic(err) + } + + initMsgBz, err := json.Marshal(initMsg) + if err != nil { + panic(err) + } + p := &StoreAndInstantiateContractProposal{ + Title: "Foo", + Description: "Bar", + RunAs: anyAddress, + WASMByteCode: wasm, + Source: "https://example.com/", + Builder: "cosmwasm/workspace-optimizer:v0.12.9", + CodeHash: codeHash, + Admin: anyAddress, + Label: "testing", + Msg: initMsgBz, + Funds: nil, + } + + for _, m := range mutators { + m(p) + } + return p +} + +func MigrateContractProposalFixture(mutators ...func(p *MigrateContractProposal)) *MigrateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, ContractAddrLen) + + migMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: anyValidAddress} + ) + + migMsgBz, err := json.Marshal(migMsg) + if err != nil { + panic(err) + } + const ( + contractAddr = "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + ) + p := &MigrateContractProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr, + CodeID: 1, + Msg: migMsgBz, + } + + for _, m := range mutators { + m(p) + } + return p +} + +func SudoContractProposalFixture(mutators ...func(p *SudoContractProposal)) *SudoContractProposal { + const ( + contractAddr = "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + ) + + p := &SudoContractProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr, + Msg: []byte(`{"do":"something"}`), + } + + for _, m := range mutators { + m(p) + } + return p +} + +func ExecuteContractProposalFixture(mutators ...func(p *ExecuteContractProposal)) *ExecuteContractProposal { + const ( + contractAddr = "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + ) + + p := &ExecuteContractProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr, + RunAs: anyAddress, + Msg: []byte(`{"do":"something"}`), + Funds: sdk.Coins{{ + Denom: "stake", + Amount: sdk.NewInt(1), + }}, + } + + for _, m := range mutators { + m(p) + } + return p +} + +func UpdateAdminProposalFixture(mutators ...func(p *UpdateAdminProposal)) *UpdateAdminProposal { + const ( + contractAddr = "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + anyAddress = "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs2m6sx4" + ) + + p := &UpdateAdminProposal{ + Title: "Foo", + Description: "Bar", + NewAdmin: anyAddress, + Contract: contractAddr, + } + for _, m := range mutators { + m(p) + } + return p +} + +func ClearAdminProposalFixture(mutators ...func(p *ClearAdminProposal)) *ClearAdminProposal { + const contractAddr = "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr" + p := &ClearAdminProposal{ + Title: "Foo", + Description: "Bar", + Contract: contractAddr, + } + for _, m := range mutators { + m(p) + } + return p +} diff --git a/x/wasm/types/tx.go b/x/wasm/types/tx.go new file mode 100644 index 00000000..045f5341 --- /dev/null +++ b/x/wasm/types/tx.go @@ -0,0 +1,447 @@ +package types + +import ( + "bytes" + "encoding/json" + "errors" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// RawContractMessage defines a json message that is sent or returned by a wasm contract. +// This type can hold any type of bytes. Until validateBasic is called there should not be +// any assumptions made that the data is valid syntax or semantic. +type RawContractMessage []byte + +func (r RawContractMessage) MarshalJSON() ([]byte, error) { + return json.RawMessage(r).MarshalJSON() +} + +func (r *RawContractMessage) UnmarshalJSON(b []byte) error { + if r == nil { + return errors.New("unmarshalJSON on nil pointer") + } + *r = append((*r)[0:0], b...) + return nil +} + +func (r *RawContractMessage) ValidateBasic() error { + if r == nil { + return ErrEmpty + } + if !json.Valid(*r) { + return ErrInvalid + } + return nil +} + +// Bytes returns raw bytes type +func (r RawContractMessage) Bytes() []byte { + return r +} + +// Equal content is equal json. Byte equal but this can change in the future. +func (r RawContractMessage) Equal(o RawContractMessage) bool { + return bytes.Equal(r.Bytes(), o.Bytes()) +} + +func (msg MsgStoreCode) Route() string { + return RouterKey +} + +func (msg MsgStoreCode) Type() string { + return "store-code" +} + +func (msg MsgStoreCode) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return err + } + + if err := validateWasmCode(msg.WASMByteCode, MaxWasmSize); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error()) + } + + if msg.InstantiatePermission != nil { + if err := msg.InstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + // AccessTypeOnlyAddress is still considered valid as legacy instantiation permission + // but not for new contracts + if msg.InstantiatePermission.Permission == AccessTypeOnlyAddress { + return ErrInvalid.Wrap("unsupported type, use AccessTypeAnyOfAddresses instead") + } + } + return nil +} + +func (msg MsgStoreCode) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgStoreCode) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +func (msg MsgInstantiateContract) Route() string { + return RouterKey +} + +func (msg MsgInstantiateContract) Type() string { + return "instantiate" +} + +func (msg MsgInstantiateContract) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + + if msg.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + + if err := ValidateLabel(msg.Label); err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "label is required") + } + + if !msg.Funds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(msg.Admin) != 0 { + if _, err := sdk.AccAddressFromBech32(msg.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + if err := msg.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +func (msg MsgInstantiateContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgInstantiateContract) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +func (msg MsgExecuteContract) Route() string { + return RouterKey +} + +func (msg MsgExecuteContract) Type() string { + return "execute" +} + +func (msg MsgExecuteContract) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if _, err := sdk.AccAddressFromBech32(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + + if !msg.Funds.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "sentFunds") + } + if err := msg.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + return nil +} + +func (msg MsgExecuteContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgExecuteContract) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +// GetMsg returns the payload message send to the contract +func (msg MsgExecuteContract) GetMsg() RawContractMessage { + return msg.Msg +} + +// GetFunds returns tokens send to the contract +func (msg MsgExecuteContract) GetFunds() sdk.Coins { + return msg.Funds +} + +// GetContract returns the bech32 address of the contract +func (msg MsgExecuteContract) GetContract() string { + return msg.Contract +} + +func (msg MsgMigrateContract) Route() string { + return RouterKey +} + +func (msg MsgMigrateContract) Type() string { + return "migrate" +} + +func (msg MsgMigrateContract) ValidateBasic() error { + if msg.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if _, err := sdk.AccAddressFromBech32(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + + if err := msg.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + + return nil +} + +func (msg MsgMigrateContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgMigrateContract) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +// GetMsg returns the payload message send to the contract +func (msg MsgMigrateContract) GetMsg() RawContractMessage { + return msg.Msg +} + +// GetFunds returns tokens send to the contract +func (msg MsgMigrateContract) GetFunds() sdk.Coins { + return sdk.NewCoins() +} + +// GetContract returns the bech32 address of the contract +func (msg MsgMigrateContract) GetContract() string { + return msg.Contract +} + +func (msg MsgUpdateAdmin) Route() string { + return RouterKey +} + +func (msg MsgUpdateAdmin) Type() string { + return "update-contract-admin" +} + +func (msg MsgUpdateAdmin) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if _, err := sdk.AccAddressFromBech32(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if _, err := sdk.AccAddressFromBech32(msg.NewAdmin); err != nil { + return sdkerrors.Wrap(err, "new admin") + } + if strings.EqualFold(msg.Sender, msg.NewAdmin) { + return sdkerrors.Wrap(ErrInvalidMsg, "new admin is the same as the old") + } + return nil +} + +func (msg MsgUpdateAdmin) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgUpdateAdmin) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +func (msg MsgClearAdmin) Route() string { + return RouterKey +} + +func (msg MsgClearAdmin) Type() string { + return "clear-contract-admin" +} + +func (msg MsgClearAdmin) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if _, err := sdk.AccAddressFromBech32(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + return nil +} + +func (msg MsgClearAdmin) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgClearAdmin) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +func (msg MsgIBCSend) Route() string { + return RouterKey +} + +func (msg MsgIBCSend) Type() string { + return "wasm-ibc-send" +} + +func (msg MsgIBCSend) ValidateBasic() error { + return nil +} + +func (msg MsgIBCSend) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgIBCSend) GetSigners() []sdk.AccAddress { + return nil +} + +func (msg MsgIBCCloseChannel) Route() string { + return RouterKey +} + +func (msg MsgIBCCloseChannel) Type() string { + return "wasm-ibc-close" +} + +func (msg MsgIBCCloseChannel) ValidateBasic() error { + return nil +} + +func (msg MsgIBCCloseChannel) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgIBCCloseChannel) GetSigners() []sdk.AccAddress { + return nil +} + +var _ sdk.Msg = &MsgInstantiateContract2{} + +func (msg MsgInstantiateContract2) Route() string { + return RouterKey +} + +func (msg MsgInstantiateContract2) Type() string { + return "instantiate2" +} + +func (msg MsgInstantiateContract2) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + + if msg.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + + if err := ValidateLabel(msg.Label); err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "label is required") + } + + if !msg.Funds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(msg.Admin) != 0 { + if _, err := sdk.AccAddressFromBech32(msg.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + if err := msg.Msg.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "payload msg") + } + if err := ValidateSalt(msg.Salt); err != nil { + return sdkerrors.Wrap(err, "salt") + } + return nil +} + +func (msg MsgInstantiateContract2) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgInstantiateContract2) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} + +func (msg MsgUpdateInstantiateConfig) Route() string { + return RouterKey +} + +func (msg MsgUpdateInstantiateConfig) Type() string { + return "update-instantiate-config" +} + +func (msg MsgUpdateInstantiateConfig) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + + if msg.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + + if msg.NewInstantiatePermission == nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "instantiate permission is required") + } + + if err := msg.NewInstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + // AccessTypeOnlyAddress is still considered valid as legacy instantiation permission + // but not for new contracts + if msg.NewInstantiatePermission.Permission == AccessTypeOnlyAddress { + return ErrInvalid.Wrap("unsupported type, use AccessTypeAnyOfAddresses instead") + } + + return nil +} + +func (msg MsgUpdateInstantiateConfig) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgUpdateInstantiateConfig) GetSigners() []sdk.AccAddress { + senderAddr, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { // should never happen as valid basic rejects invalid addresses + panic(err.Error()) + } + return []sdk.AccAddress{senderAddr} +} diff --git a/x/wasm/types/tx_test.go b/x/wasm/types/tx_test.go new file mode 100644 index 00000000..57e36a46 --- /dev/null +++ b/x/wasm/types/tx_test.go @@ -0,0 +1,751 @@ +package types + +import ( + "bytes" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const firstCodeID = 1 + +func TestStoreCodeValidation(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, ContractAddrLen)).String() + sdk.GetConfig().SetAddressVerifier(VerifyAddressLen()) + cases := map[string]struct { + msg MsgStoreCode + valid bool + }{ + "empty": { + msg: MsgStoreCode{}, + valid: false, + }, + "correct minimal": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + }, + valid: true, + }, + "missing code": { + msg: MsgStoreCode{ + Sender: goodAddress, + }, + valid: false, + }, + "bad sender minimal": { + msg: MsgStoreCode{ + Sender: badAddress, + WASMByteCode: []byte("foo"), + }, + valid: false, + }, + "correct maximal": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + }, + valid: true, + }, + "invalid InstantiatePermission": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + InstantiatePermission: &AccessConfig{Permission: AccessTypeOnlyAddress, Address: badAddress}, + }, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestInstantiateContractValidation(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + sdk.GetConfig().SetAddressVerifier(VerifyAddressLen()) + + cases := map[string]struct { + msg MsgInstantiateContract + valid bool + }{ + "empty": { + msg: MsgInstantiateContract{}, + valid: false, + }, + "correct minimal": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte("{}"), + }, + valid: true, + }, + "missing code": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + Label: "foo", + Msg: []byte("{}"), + }, + valid: false, + }, + "missing label": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + Msg: []byte("{}"), + }, + valid: false, + }, + "label too long": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + Label: strings.Repeat("food", 33), + }, + valid: false, + }, + "bad sender minimal": { + msg: MsgInstantiateContract{ + Sender: badAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte("{}"), + }, + valid: false, + }, + "correct maximal": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte(`{"some": "data"}`), + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(200)}}, + }, + valid: true, + }, + "negative funds": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte(`{"some": "data"}`), + // we cannot use sdk.NewCoin() constructors as they panic on creating invalid data (before we can test) + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(-200)}}, + }, + valid: false, + }, + "non json init msg": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte("invalid-json"), + }, + valid: false, + }, + "empty init msg": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + }, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestInstantiateContract2Validation(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + sdk.GetConfig().SetAddressVerifier(VerifyAddressLen()) + + cases := map[string]struct { + msg MsgInstantiateContract2 + valid bool + }{ + "empty": { + msg: MsgInstantiateContract2{}, + valid: false, + }, + "correct minimal": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte("{}"), + Salt: []byte{0}, + }, + valid: true, + }, + "missing code": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + Label: "foo", + Msg: []byte("{}"), + Salt: []byte{0}, + }, + valid: false, + }, + "missing label": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + Msg: []byte("{}"), + Salt: []byte{0}, + }, + valid: false, + }, + "label too long": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + Label: strings.Repeat("food", 33), + Salt: []byte{0}, + }, + valid: false, + }, + "bad sender minimal": { + msg: MsgInstantiateContract2{ + Sender: badAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte("{}"), + Salt: []byte{0}, + }, + valid: false, + }, + "correct maximal": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: strings.Repeat("a", MaxLabelSize), + Msg: []byte(`{"some": "data"}`), + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(200)}}, + Salt: bytes.Repeat([]byte{0}, MaxSaltSize), + FixMsg: true, + }, + valid: true, + }, + "negative funds": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte(`{"some": "data"}`), + // we cannot use sdk.NewCoin() constructors as they panic on creating invalid data (before we can test) + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(-200)}}, + Salt: []byte{0}, + }, + valid: false, + }, + "non json init msg": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte("invalid-json"), + Salt: []byte{0}, + }, + valid: false, + }, + "empty init msg": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Salt: []byte{0}, + }, + valid: false, + }, + "empty salt": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "salt too long": { + msg: MsgInstantiateContract2{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + Msg: []byte(`{"some": "data"}`), + Salt: bytes.Repeat([]byte{0}, 65), + }, + valid: false, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestExecuteContractValidation(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + + cases := map[string]struct { + msg MsgExecuteContract + valid bool + }{ + "empty": { + msg: MsgExecuteContract{}, + valid: false, + }, + "correct minimal": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte("{}"), + }, + valid: true, + }, + "correct all": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(200)}}, + }, + valid: true, + }, + "bad sender": { + msg: MsgExecuteContract{ + Sender: badAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "empty sender": { + msg: MsgExecuteContract{ + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "bad contract": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: badAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "empty contract": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "negative funds": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(-1)}}, + }, + valid: false, + }, + "duplicate funds": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + Funds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(1)}, sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(1)}}, + }, + valid: false, + }, + "non json msg": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte("invalid-json"), + }, + valid: false, + }, + "empty msg": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + }, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestMsgUpdateAdministrator(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + otherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x1}, 20)).String() + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)).String() + + specs := map[string]struct { + src MsgUpdateAdmin + expErr bool + }{ + "all good": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: otherGoodAddress, + Contract: anotherGoodAddress, + }, + }, + "new admin required": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad sender": { + src: MsgUpdateAdmin{ + Sender: badAddress, + NewAdmin: otherGoodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad new admin": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: badAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad contract addr": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: otherGoodAddress, + Contract: badAddress, + }, + expErr: true, + }, + "new admin same as old admin": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: goodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestMsgClearAdministrator(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)).String() + + specs := map[string]struct { + src MsgClearAdmin + expErr bool + }{ + "all good": { + src: MsgClearAdmin{ + Sender: goodAddress, + Contract: anotherGoodAddress, + }, + }, + "bad sender": { + src: MsgClearAdmin{ + Sender: badAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad contract addr": { + src: MsgClearAdmin{ + Sender: goodAddress, + Contract: badAddress, + }, + expErr: true, + }, + "contract missing": { + src: MsgClearAdmin{ + Sender: goodAddress, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestMsgMigrateContract(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)).String() + + specs := map[string]struct { + src MsgMigrateContract + expErr bool + }{ + "all good": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + Msg: []byte("{}"), + }, + }, + "bad sender": { + src: MsgMigrateContract{ + Sender: badAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "empty sender": { + src: MsgMigrateContract{ + Contract: anotherGoodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "empty code": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad contract addr": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: badAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "empty contract addr": { + src: MsgMigrateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "non json migrateMsg": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + Msg: []byte("invalid json"), + }, + expErr: true, + }, + "empty migrateMsg": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestMsgJsonSignBytes(t *testing.T) { + const myInnerMsg = `{"foo":"bar"}` + specs := map[string]struct { + src legacytx.LegacyMsg + exp string + }{ + "MsgInstantiateContract": { + src: &MsgInstantiateContract{Msg: RawContractMessage(myInnerMsg)}, + exp: ` +{ + "type":"wasm/MsgInstantiateContract", + "value": {"msg": {"foo":"bar"}, "funds":[]} +}`, + }, + "MsgExecuteContract": { + src: &MsgExecuteContract{Msg: RawContractMessage(myInnerMsg)}, + exp: ` +{ + "type":"wasm/MsgExecuteContract", + "value": {"msg": {"foo":"bar"}, "funds":[]} +}`, + }, + "MsgMigrateContract": { + src: &MsgMigrateContract{Msg: RawContractMessage(myInnerMsg)}, + exp: ` +{ + "type":"wasm/MsgMigrateContract", + "value": {"msg": {"foo":"bar"}} +}`, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + bz := spec.src.GetSignBytes() + assert.JSONEq(t, spec.exp, string(bz), "raw: %s", string(bz)) + }) + } +} + +func TestMsgUpdateInstantiateConfig(t *testing.T) { + bad, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + badAddress := bad.String() + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)).String() + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)).String() + + specs := map[string]struct { + src MsgUpdateInstantiateConfig + expErr bool + }{ + "all good": { + src: MsgUpdateInstantiateConfig{ + Sender: goodAddress, + CodeID: 1, + NewInstantiatePermission: &AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{anotherGoodAddress}}, + }, + }, + "retained AccessTypeOnlyAddress": { + src: MsgUpdateInstantiateConfig{ + Sender: goodAddress, + CodeID: 1, + NewInstantiatePermission: &AccessConfig{Permission: AccessTypeOnlyAddress, Address: anotherGoodAddress}, + }, + expErr: true, + }, + "bad sender": { + src: MsgUpdateInstantiateConfig{ + Sender: badAddress, + CodeID: 1, + NewInstantiatePermission: &AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{anotherGoodAddress}}, + }, + expErr: true, + }, + "invalid NewInstantiatePermission": { + src: MsgUpdateInstantiateConfig{ + Sender: goodAddress, + CodeID: 1, + NewInstantiatePermission: &AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{badAddress}}, + }, + expErr: true, + }, + "missing code id": { + src: MsgUpdateInstantiateConfig{ + Sender: goodAddress, + NewInstantiatePermission: &AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{anotherGoodAddress}}, + }, + expErr: true, + }, + "missing NewInstantiatePermission": { + src: MsgUpdateInstantiateConfig{ + Sender: goodAddress, + CodeID: 1, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/x/wasm/types/types.go b/x/wasm/types/types.go new file mode 100644 index 00000000..4a0e198f --- /dev/null +++ b/x/wasm/types/types.go @@ -0,0 +1,398 @@ +package types + +import ( + "fmt" + "reflect" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/gogo/protobuf/proto" +) + +const ( + defaultMemoryCacheSize uint32 = 100 // in MiB + defaultSmartQueryGasLimit uint64 = 3_000_000 + defaultContractDebugMode = false + + // ContractAddrLen defines a valid address length for contracts + ContractAddrLen = 32 + // SDKAddrLen defines a valid address length that was used in sdk address generation + SDKAddrLen = 20 +) + +func (m Model) ValidateBasic() error { + if len(m.Key) == 0 { + return sdkerrors.Wrap(ErrEmpty, "key") + } + return nil +} + +func (c CodeInfo) ValidateBasic() error { + if len(c.CodeHash) == 0 { + return sdkerrors.Wrap(ErrEmpty, "code hash") + } + if _, err := sdk.AccAddressFromBech32(c.Creator); err != nil { + return sdkerrors.Wrap(err, "creator") + } + if err := c.InstantiateConfig.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate config") + } + return nil +} + +// NewCodeInfo fills a new CodeInfo struct +func NewCodeInfo(codeHash []byte, creator sdk.AccAddress, instantiatePermission AccessConfig) CodeInfo { + return CodeInfo{ + CodeHash: codeHash, + Creator: creator.String(), + InstantiateConfig: instantiatePermission, + } +} + +var AllCodeHistoryTypes = []ContractCodeHistoryOperationType{ContractCodeHistoryOperationTypeGenesis, ContractCodeHistoryOperationTypeInit, ContractCodeHistoryOperationTypeMigrate} + +// NewContractInfo creates a new instance of a given WASM contract info +func NewContractInfo(codeID uint64, creator, admin sdk.AccAddress, label string, createdAt *AbsoluteTxPosition) ContractInfo { + var adminAddr string + if !admin.Empty() { + adminAddr = admin.String() + } + return ContractInfo{ + CodeID: codeID, + Creator: creator.String(), + Admin: adminAddr, + Label: label, + Created: createdAt, + } +} + +// validatable is an optional interface that can be implemented by an ContractInfoExtension to enable validation +type validatable interface { + ValidateBasic() error +} + +// ValidateBasic does syntax checks on the data. If an extension is set and has the `ValidateBasic() error` method, then +// the method is called as well. It is recommend to implement `ValidateBasic` so that the data is verified in the setter +// but also in the genesis import process. +func (c *ContractInfo) ValidateBasic() error { + if c.CodeID == 0 { + return sdkerrors.Wrap(ErrEmpty, "code id") + } + if _, err := sdk.AccAddressFromBech32(c.Creator); err != nil { + return sdkerrors.Wrap(err, "creator") + } + if len(c.Admin) != 0 { + if _, err := sdk.AccAddressFromBech32(c.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + if err := ValidateLabel(c.Label); err != nil { + return sdkerrors.Wrap(err, "label") + } + if c.Extension == nil { + return nil + } + + e, ok := c.Extension.GetCachedValue().(validatable) + if !ok { + return nil + } + if err := e.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "extension") + } + return nil +} + +// SetExtension set new extension data. Calls `ValidateBasic() error` on non nil values when method is implemented by +// the extension. +func (c *ContractInfo) SetExtension(ext ContractInfoExtension) error { + if ext == nil { + c.Extension = nil + return nil + } + if e, ok := ext.(validatable); ok { + if err := e.ValidateBasic(); err != nil { + return err + } + } + any, err := codectypes.NewAnyWithValue(ext) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrPackAny, err.Error()) + } + + c.Extension = any + return nil +} + +// ReadExtension copies the extension value to the pointer passed as argument so that there is no need to cast +// For example with a custom extension of type `MyContractDetails` it will look as following: +// +// var d MyContractDetails +// if err := info.ReadExtension(&d); err != nil { +// return nil, sdkerrors.Wrap(err, "extension") +// } +func (c *ContractInfo) ReadExtension(e ContractInfoExtension) error { + rv := reflect.ValueOf(e) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidType, "not a pointer") + } + if c.Extension == nil { + return nil + } + + cached := c.Extension.GetCachedValue() + elem := reflect.ValueOf(cached).Elem() + if !elem.Type().AssignableTo(rv.Elem().Type()) { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "extension is of type %s but argument of %s", elem.Type(), rv.Elem().Type()) + } + rv.Elem().Set(elem) + return nil +} + +func (c ContractInfo) InitialHistory(initMsg []byte) ContractCodeHistoryEntry { + return ContractCodeHistoryEntry{ + Operation: ContractCodeHistoryOperationTypeInit, + CodeID: c.CodeID, + Updated: c.Created, + Msg: initMsg, + } +} + +func (c *ContractInfo) AddMigration(ctx sdk.Context, codeID uint64, msg []byte) ContractCodeHistoryEntry { + h := ContractCodeHistoryEntry{ + Operation: ContractCodeHistoryOperationTypeMigrate, + CodeID: codeID, + Updated: NewAbsoluteTxPosition(ctx), + Msg: msg, + } + c.CodeID = codeID + return h +} + +// AdminAddr convert into sdk.AccAddress or nil when not set +func (c *ContractInfo) AdminAddr() sdk.AccAddress { + if c.Admin == "" { + return nil + } + admin, err := sdk.AccAddressFromBech32(c.Admin) + if err != nil { // should never happen + panic(err.Error()) + } + return admin +} + +// ContractInfoExtension defines the extension point for custom data to be stored with a contract info +type ContractInfoExtension interface { + proto.Message + String() string +} + +var _ codectypes.UnpackInterfacesMessage = &ContractInfo{} + +// UnpackInterfaces implements codectypes.UnpackInterfaces +func (c *ContractInfo) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + var details ContractInfoExtension + if err := unpacker.UnpackAny(c.Extension, &details); err != nil { + return err + } + return codectypes.UnpackInterfaces(details, unpacker) +} + +// NewAbsoluteTxPosition gets a block position from the context +func NewAbsoluteTxPosition(ctx sdk.Context) *AbsoluteTxPosition { + // we must safely handle nil gas meters + var index uint64 + meter := ctx.BlockGasMeter() + if meter != nil { + index = meter.GasConsumed() + } + height := ctx.BlockHeight() + if height < 0 { + panic(fmt.Sprintf("unsupported height: %d", height)) + } + return &AbsoluteTxPosition{ + BlockHeight: uint64(height), + TxIndex: index, + } +} + +// LessThan can be used to sort +func (a *AbsoluteTxPosition) LessThan(b *AbsoluteTxPosition) bool { + if a == nil { + return true + } + if b == nil { + return false + } + return a.BlockHeight < b.BlockHeight || (a.BlockHeight == b.BlockHeight && a.TxIndex < b.TxIndex) +} + +// AbsoluteTxPositionLen number of elements in byte representation +const AbsoluteTxPositionLen = 16 + +// Bytes encodes the object into a 16 byte representation with big endian block height and tx index. +func (a *AbsoluteTxPosition) Bytes() []byte { + if a == nil { + panic("object must not be nil") + } + r := make([]byte, AbsoluteTxPositionLen) + copy(r[0:], sdk.Uint64ToBigEndian(a.BlockHeight)) + copy(r[8:], sdk.Uint64ToBigEndian(a.TxIndex)) + return r +} + +// ValidateBasic syntax checks +func (c ContractCodeHistoryEntry) ValidateBasic() error { + var found bool + for _, v := range AllCodeHistoryTypes { + if c.Operation == v { + found = true + break + } + } + if !found { + return ErrInvalid.Wrap("operation") + } + if c.CodeID == 0 { + return ErrEmpty.Wrap("code id") + } + if c.Updated == nil { + return ErrEmpty.Wrap("updated") + } + return sdkerrors.Wrap(c.Msg.ValidateBasic(), "msg") +} + +// NewEnv initializes the environment for a contract instance +func NewEnv(ctx sdk.Context, contractAddr sdk.AccAddress) wasmvmtypes.Env { + // safety checks before casting below + if ctx.BlockHeight() < 0 { + panic("Block height must never be negative") + } + nano := ctx.BlockTime().UnixNano() + if nano < 1 { + panic("Block (unix) time must never be empty or negative ") + } + + env := wasmvmtypes.Env{ + Block: wasmvmtypes.BlockInfo{ + Height: uint64(ctx.BlockHeight()), + Time: uint64(nano), + ChainID: ctx.ChainID(), + }, + Contract: wasmvmtypes.ContractInfo{ + Address: contractAddr.String(), + }, + } + if txCounter, ok := TXCounter(ctx); ok { + env.Transaction = &wasmvmtypes.TransactionInfo{Index: txCounter} + } + return env +} + +// NewInfo initializes the MessageInfo for a contract instance +func NewInfo(creator sdk.AccAddress, deposit sdk.Coins) wasmvmtypes.MessageInfo { + return wasmvmtypes.MessageInfo{ + Sender: creator.String(), + Funds: NewWasmCoins(deposit), + } +} + +// NewWasmCoins translates between Cosmos SDK coins and Wasm coins +func NewWasmCoins(cosmosCoins sdk.Coins) (wasmCoins []wasmvmtypes.Coin) { + for _, coin := range cosmosCoins { + wasmCoin := wasmvmtypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + } + wasmCoins = append(wasmCoins, wasmCoin) + } + return wasmCoins +} + +// WasmConfig is the extra config required for wasm +type WasmConfig struct { + // SimulationGasLimit is the max gas to be used in a tx simulation call. + // When not set the consensus max block gas is used instead + SimulationGasLimit *uint64 + // SimulationGasLimit is the max gas to be used in a smart query contract call + SmartQueryGasLimit uint64 + // MemoryCacheSize in MiB not bytes + MemoryCacheSize uint32 + // ContractDebugMode log what contract print + ContractDebugMode bool +} + +// DefaultWasmConfig returns the default settings for WasmConfig +func DefaultWasmConfig() WasmConfig { + return WasmConfig{ + SmartQueryGasLimit: defaultSmartQueryGasLimit, + MemoryCacheSize: defaultMemoryCacheSize, + ContractDebugMode: defaultContractDebugMode, + } +} + +// VerifyAddressLen ensures that the address matches the expected length +func VerifyAddressLen() func(addr []byte) error { + return func(addr []byte) error { + if len(addr) != ContractAddrLen && len(addr) != SDKAddrLen { + return sdkerrors.ErrInvalidAddress + } + return nil + } +} + +// IsSubset will return true if the caller is the same as the superset, +// or if the caller is more restrictive than the superset. +func (a AccessType) IsSubset(superSet AccessType) bool { + switch superSet { + case AccessTypeEverybody: + // Everything is a subset of this + return a != AccessTypeUnspecified + case AccessTypeNobody: + // Only an exact match is a subset of this + return a == AccessTypeNobody + case AccessTypeOnlyAddress, AccessTypeAnyOfAddresses: + // Nobody or address(es) + return a == AccessTypeNobody || a == AccessTypeOnlyAddress || a == AccessTypeAnyOfAddresses + default: + return false + } +} + +// IsSubset will return true if the caller is the same as the superset, +// or if the caller is more restrictive than the superset. +func (a AccessConfig) IsSubset(superSet AccessConfig) bool { + switch superSet.Permission { + case AccessTypeOnlyAddress: + // An exact match or nobody + return a.Permission == AccessTypeNobody || (a.Permission == AccessTypeOnlyAddress && a.Address == superSet.Address) || + (a.Permission == AccessTypeAnyOfAddresses && isSubset([]string{superSet.Address}, a.Addresses)) + case AccessTypeAnyOfAddresses: + // An exact match or nobody + return a.Permission == AccessTypeNobody || (a.Permission == AccessTypeOnlyAddress && isSubset(superSet.Addresses, []string{a.Address})) || + a.Permission == AccessTypeAnyOfAddresses && isSubset(superSet.Addresses, a.Addresses) + case AccessTypeUnspecified: + return false + default: + return a.Permission.IsSubset(superSet.Permission) + } +} + +// return true when all elements in sub are also part of super +func isSubset(super, sub []string) bool { + if len(sub) == 0 { + return true + } + var matches int + for _, o := range sub { + for _, s := range super { + if o == s { + matches++ + break + } + } + } + return matches == len(sub) +} diff --git a/x/wasm/types/types_test.go b/x/wasm/types/types_test.go new file mode 100644 index 00000000..25df8280 --- /dev/null +++ b/x/wasm/types/types_test.go @@ -0,0 +1,747 @@ +package types + +import ( + "bytes" + "context" + "strings" + "testing" + "time" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/rand" +) + +func TestContractInfoValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*ContractInfo) + expError bool + }{ + "all good": {srcMutator: func(_ *ContractInfo) {}}, + "code id empty": { + srcMutator: func(c *ContractInfo) { c.CodeID = 0 }, + expError: true, + }, + "creator empty": { + srcMutator: func(c *ContractInfo) { c.Creator = "" }, + expError: true, + }, + "creator not an address": { + srcMutator: func(c *ContractInfo) { c.Creator = "invalid address" }, + expError: true, + }, + "admin empty": { + srcMutator: func(c *ContractInfo) { c.Admin = "" }, + expError: false, + }, + "admin not an address": { + srcMutator: func(c *ContractInfo) { c.Admin = "invalid address" }, + expError: true, + }, + "label empty": { + srcMutator: func(c *ContractInfo) { c.Label = "" }, + expError: true, + }, + "label exceeds limit": { + srcMutator: func(c *ContractInfo) { c.Label = strings.Repeat("a", MaxLabelSize+1) }, + expError: true, + }, + "invalid extension": { + srcMutator: func(c *ContractInfo) { + // any protobuf type with ValidateBasic method + any, err := codectypes.NewAnyWithValue(&govtypes.TextProposal{}) + require.NoError(t, err) + c.Extension = any + }, + expError: true, + }, + "not validatable extension": { + srcMutator: func(c *ContractInfo) { + // any protobuf type with ValidateBasic method + any, err := codectypes.NewAnyWithValue(&govtypes.Proposal{}) + require.NoError(t, err) + c.Extension = any + }, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := ContractInfoFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestCodeInfoValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*CodeInfo) + expError bool + }{ + "all good": {srcMutator: func(_ *CodeInfo) {}}, + "code hash empty": { + srcMutator: func(c *CodeInfo) { c.CodeHash = []byte{} }, + expError: true, + }, + "code hash nil": { + srcMutator: func(c *CodeInfo) { c.CodeHash = nil }, + expError: true, + }, + "creator empty": { + srcMutator: func(c *CodeInfo) { c.Creator = "" }, + expError: true, + }, + "creator not an address": { + srcMutator: func(c *CodeInfo) { c.Creator = "invalid address" }, + expError: true, + }, + "Instantiate config invalid": { + srcMutator: func(c *CodeInfo) { c.InstantiateConfig = AccessConfig{} }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := CodeInfoFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestContractInfoSetExtension(t *testing.T) { + anyTime := time.Now().UTC() + aNestedProtobufExt := func() ContractInfoExtension { + // using gov proposal here as a random protobuf types as it contains an Any type inside for nested unpacking + myExtension, err := govtypes.NewProposal(&govtypes.TextProposal{Title: "bar"}, 1, anyTime, anyTime) + require.NoError(t, err) + myExtension.TotalDeposit = nil + return &myExtension + } + + specs := map[string]struct { + src ContractInfoExtension + expErr bool + expNil bool + }{ + "all good with any proto extension": { + src: aNestedProtobufExt(), + }, + "nil allowed": { + src: nil, + expNil: true, + }, + "validated and accepted": { + src: &govtypes.TextProposal{Title: "bar", Description: "set"}, + }, + "validated and rejected": { + src: &govtypes.TextProposal{Title: "bar"}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var c ContractInfo + gotErr := c.SetExtension(spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + if spec.expNil { + return + } + require.NotNil(t, c.Extension) + assert.NotNil(t, c.Extension.GetCachedValue()) + }) + } +} + +func TestContractInfoMarshalUnmarshal(t *testing.T) { + var myAddr sdk.AccAddress = rand.Bytes(ContractAddrLen) + var myOtherAddr sdk.AccAddress = rand.Bytes(ContractAddrLen) + anyPos := AbsoluteTxPosition{BlockHeight: 1, TxIndex: 2} + + anyTime := time.Now().UTC() + // using gov proposal here as a random protobuf types as it contains an Any type inside for nested unpacking + myExtension, err := govtypes.NewProposal(&govtypes.TextProposal{Title: "bar"}, 1, anyTime, anyTime) + require.NoError(t, err) + myExtension.TotalDeposit = nil + + src := NewContractInfo(1, myAddr, myOtherAddr, "bar", &anyPos) + err = src.SetExtension(&myExtension) + require.NoError(t, err) + + interfaceRegistry := types.NewInterfaceRegistry() + marshaler := codec.NewProtoCodec(interfaceRegistry) + RegisterInterfaces(interfaceRegistry) + // register proposal as extension type + interfaceRegistry.RegisterImplementations( + (*ContractInfoExtension)(nil), + &govtypes.Proposal{}, + ) + // register gov types for nested Anys + govtypes.RegisterInterfaces(interfaceRegistry) + + // when encode + bz, err := marshaler.Marshal(&src) + require.NoError(t, err) + // and decode + var dest ContractInfo + err = marshaler.Unmarshal(bz, &dest) + // then + require.NoError(t, err) + assert.Equal(t, src, dest) + // and sanity check nested any + var destExt govtypes.Proposal + require.NoError(t, dest.ReadExtension(&destExt)) + assert.Equal(t, destExt.GetTitle(), "bar") +} + +func TestContractInfoReadExtension(t *testing.T) { + anyTime := time.Now().UTC() + myExtension, err := govtypes.NewProposal(&govtypes.TextProposal{Title: "foo"}, 1, anyTime, anyTime) + require.NoError(t, err) + type TestExtensionAsStruct struct { + ContractInfoExtension + } + + specs := map[string]struct { + setup func(*ContractInfo) + param func() ContractInfoExtension + expVal ContractInfoExtension + expErr bool + }{ + "all good": { + setup: func(i *ContractInfo) { + i.SetExtension(&myExtension) + }, + param: func() ContractInfoExtension { + return &govtypes.Proposal{} + }, + expVal: &myExtension, + }, + "no extension set": { + setup: func(i *ContractInfo) { + }, + param: func() ContractInfoExtension { + return &govtypes.Proposal{} + }, + expVal: &govtypes.Proposal{}, + }, + "nil argument value": { + setup: func(i *ContractInfo) { + i.SetExtension(&myExtension) + }, + param: func() ContractInfoExtension { + return nil + }, + expErr: true, + }, + "non matching types": { + setup: func(i *ContractInfo) { + i.SetExtension(&myExtension) + }, + param: func() ContractInfoExtension { + return &govtypes.TextProposal{} + }, + expErr: true, + }, + "not a pointer argument": { + setup: func(i *ContractInfo) { + }, + param: func() ContractInfoExtension { + return TestExtensionAsStruct{} + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + var c ContractInfo + spec.setup(&c) + // when + + gotValue := spec.param() + gotErr := c.ReadExtension(gotValue) + + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expVal, gotValue) + }) + } +} + +func TestNewEnv(t *testing.T) { + myTime := time.Unix(0, 1619700924259075000) + t.Logf("++ unix: %d", myTime.UnixNano()) + var myContractAddr sdk.AccAddress = randBytes(ContractAddrLen) + specs := map[string]struct { + srcCtx sdk.Context + exp wasmvmtypes.Env + }{ + "all good with tx counter": { + srcCtx: WithTXCounter(sdk.Context{}.WithBlockHeight(1).WithBlockTime(myTime).WithChainID("testing").WithContext(context.Background()), 0), + exp: wasmvmtypes.Env{ + Block: wasmvmtypes.BlockInfo{ + Height: 1, + Time: 1619700924259075000, + ChainID: "testing", + }, + Contract: wasmvmtypes.ContractInfo{ + Address: myContractAddr.String(), + }, + Transaction: &wasmvmtypes.TransactionInfo{Index: 0}, + }, + }, + "without tx counter": { + srcCtx: sdk.Context{}.WithBlockHeight(1).WithBlockTime(myTime).WithChainID("testing").WithContext(context.Background()), + exp: wasmvmtypes.Env{ + Block: wasmvmtypes.BlockInfo{ + Height: 1, + Time: 1619700924259075000, + ChainID: "testing", + }, + Contract: wasmvmtypes.ContractInfo{ + Address: myContractAddr.String(), + }, + }, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + assert.Equal(t, spec.exp, NewEnv(spec.srcCtx, myContractAddr)) + }) + } +} + +func TestVerifyAddressLen(t *testing.T) { + specs := map[string]struct { + src []byte + expErr bool + }{ + "valid contract address": { + src: bytes.Repeat([]byte{1}, 32), + }, + "valid legacy address": { + src: bytes.Repeat([]byte{1}, 20), + }, + "address too short for legacy": { + src: bytes.Repeat([]byte{1}, 19), + expErr: true, + }, + "address too short for contract": { + src: bytes.Repeat([]byte{1}, 31), + expErr: true, + }, + "address too long for legacy": { + src: bytes.Repeat([]byte{1}, 21), + expErr: true, + }, + "address too long for contract": { + src: bytes.Repeat([]byte{1}, 33), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := VerifyAddressLen()(spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestAccessConfigSubset(t *testing.T) { + // read + // <, <= is subset of + // !< is not subset of + specs := map[string]struct { + check AccessConfig + superSet AccessConfig + isSubSet bool + }{ + // nobody + "nobody <= nobody": { + superSet: AccessConfig{Permission: AccessTypeNobody}, + check: AccessConfig{Permission: AccessTypeNobody}, + isSubSet: true, + }, + "only !< nobody": { + superSet: AccessConfig{Permission: AccessTypeNobody}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "foobar"}, + isSubSet: false, + }, + "anyOf !< nobody": { + superSet: AccessConfig{Permission: AccessTypeNobody}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"foobar"}}, + isSubSet: false, + }, + "everybody !< nobody ": { + superSet: AccessConfig{Permission: AccessTypeNobody}, + check: AccessConfig{Permission: AccessTypeEverybody}, + isSubSet: false, + }, + "unspecified !< nobody": { + superSet: AccessConfig{Permission: AccessTypeNobody}, + check: AccessConfig{Permission: AccessTypeUnspecified}, + isSubSet: false, + }, + // only + "nobody < only": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeNobody}, + isSubSet: true, + }, + "only <= only(same)": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + isSubSet: true, + }, + "only !< only(other)": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "other"}, + isSubSet: false, + }, + "anyOf(same) <= only": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + isSubSet: true, + }, + "anyOf(other) !< only": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"foobar"}}, + isSubSet: false, + }, + "anyOf(same, other) !< only": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "foobar"}}, + isSubSet: false, + }, + "everybody !< only": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeEverybody}, + isSubSet: false, + }, + "unspecified !<= only": { + superSet: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + check: AccessConfig{Permission: AccessTypeUnspecified}, + isSubSet: false, + }, + + // any of + "nobody < anyOf": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeNobody}, + isSubSet: true, + }, + "only(same) < anyOf(same)": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + isSubSet: true, + }, + "only(same) < anyOf(same, other)": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "other"}}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "owner"}, + isSubSet: true, + }, + "only(other) !< anyOf": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "other"}, + isSubSet: false, + }, + "anyOf < anyOf": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + isSubSet: true, + }, + "anyOf(multiple) < anyOf(multiple)": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "other"}}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "other"}}, + isSubSet: true, + }, + "anyOf(multiple, other) !< anyOf(multiple)": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "other", "foo"}}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "other", "bar"}}, + isSubSet: false, + }, + "anyOf(multiple) !< anyOf": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner", "other"}}, + isSubSet: false, + }, + "everybody !< anyOf": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeEverybody}, + isSubSet: false, + }, + "unspecified !< anyOf ": { + superSet: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"owner"}}, + check: AccessConfig{Permission: AccessTypeUnspecified}, + isSubSet: false, + }, + // everybody + "nobody < everybody": { + superSet: AccessConfig{Permission: AccessTypeEverybody}, + check: AccessConfig{Permission: AccessTypeNobody}, + isSubSet: true, + }, + "only < everybody": { + superSet: AccessConfig{Permission: AccessTypeEverybody}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "foobar"}, + isSubSet: true, + }, + "anyOf < everybody": { + superSet: AccessConfig{Permission: AccessTypeEverybody}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"foobar"}}, + isSubSet: true, + }, + "everybody <= everybody": { + superSet: AccessConfig{Permission: AccessTypeEverybody}, + check: AccessConfig{Permission: AccessTypeEverybody}, + isSubSet: true, + }, + "unspecified !< everybody ": { + superSet: AccessConfig{Permission: AccessTypeEverybody}, + check: AccessConfig{Permission: AccessTypeUnspecified}, + isSubSet: false, + }, + // unspecified + "nobody !< unspecified": { + superSet: AccessConfig{Permission: AccessTypeUnspecified}, + check: AccessConfig{Permission: AccessTypeNobody}, + isSubSet: false, + }, + "only !< unspecified": { + superSet: AccessConfig{Permission: AccessTypeUnspecified}, + check: AccessConfig{Permission: AccessTypeOnlyAddress, Address: "foobar"}, + isSubSet: false, + }, + "anyOf !< unspecified": { + superSet: AccessConfig{Permission: AccessTypeUnspecified}, + check: AccessConfig{Permission: AccessTypeAnyOfAddresses, Addresses: []string{"foobar"}}, + isSubSet: false, + }, + "everybody !< unspecified": { + superSet: AccessConfig{Permission: AccessTypeUnspecified}, + check: AccessConfig{Permission: AccessTypeEverybody}, + isSubSet: false, + }, + "unspecified !< unspecified ": { + superSet: AccessConfig{Permission: AccessTypeUnspecified}, + check: AccessConfig{Permission: AccessTypeUnspecified}, + isSubSet: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + subset := spec.check.IsSubset(spec.superSet) + require.Equal(t, spec.isSubSet, subset) + }) + } +} + +func TestAccessTypeSubset(t *testing.T) { + specs := map[string]struct { + check AccessType + superSet AccessType + isSubSet bool + }{ + // nobody + "nobody <= nobody": { + superSet: AccessTypeNobody, + check: AccessTypeNobody, + isSubSet: true, + }, + "only !< nobody": { + superSet: AccessTypeNobody, + check: AccessTypeOnlyAddress, + isSubSet: false, + }, + "any !< nobody": { + superSet: AccessTypeNobody, + check: AccessTypeAnyOfAddresses, + isSubSet: false, + }, + "everybody !< nobody": { + superSet: AccessTypeNobody, + check: AccessTypeEverybody, + isSubSet: false, + }, + "unspecified !< nobody": { + superSet: AccessTypeNobody, + check: AccessTypeUnspecified, + isSubSet: false, + }, + // only + "nobody < only": { + superSet: AccessTypeOnlyAddress, + check: AccessTypeNobody, + isSubSet: true, + }, + "only <= only": { + superSet: AccessTypeOnlyAddress, + check: AccessTypeOnlyAddress, + isSubSet: true, + }, + "anyOf !< only": { + superSet: AccessTypeOnlyAddress, + check: AccessTypeAnyOfAddresses, + isSubSet: true, + }, + "everybody !< only": { + superSet: AccessTypeOnlyAddress, + check: AccessTypeEverybody, + isSubSet: false, + }, + "unspecified !< only": { + superSet: AccessTypeOnlyAddress, + check: AccessTypeUnspecified, + isSubSet: false, + }, + // any of + "nobody < anyOf": { + superSet: AccessTypeAnyOfAddresses, + check: AccessTypeNobody, + isSubSet: true, + }, + "only <= anyOf": { + superSet: AccessTypeAnyOfAddresses, + check: AccessTypeOnlyAddress, + isSubSet: true, + }, + "anyOf <= anyOf": { + superSet: AccessTypeAnyOfAddresses, + check: AccessTypeAnyOfAddresses, + isSubSet: true, + }, + "everybody !< anyOf": { + superSet: AccessTypeAnyOfAddresses, + check: AccessTypeEverybody, + isSubSet: false, + }, + "unspecified !< anyOf": { + superSet: AccessTypeAnyOfAddresses, + check: AccessTypeUnspecified, + isSubSet: false, + }, + // everybody + "nobody < everybody": { + superSet: AccessTypeEverybody, + check: AccessTypeNobody, + isSubSet: true, + }, + "only < everybody": { + superSet: AccessTypeEverybody, + check: AccessTypeOnlyAddress, + isSubSet: true, + }, + "anyOf < everybody": { + superSet: AccessTypeEverybody, + check: AccessTypeAnyOfAddresses, + isSubSet: true, + }, + "everybody <= everybody": { + superSet: AccessTypeEverybody, + check: AccessTypeEverybody, + isSubSet: true, + }, + "unspecified !< everybody": { + superSet: AccessTypeEverybody, + check: AccessTypeUnspecified, + isSubSet: false, + }, + // unspecified + "nobody !< unspecified": { + superSet: AccessTypeUnspecified, + check: AccessTypeNobody, + isSubSet: false, + }, + "only !< unspecified": { + superSet: AccessTypeUnspecified, + check: AccessTypeOnlyAddress, + isSubSet: false, + }, + "anyOf !< unspecified": { + superSet: AccessTypeUnspecified, + check: AccessTypeAnyOfAddresses, + isSubSet: false, + }, + "everybody !< unspecified": { + superSet: AccessTypeUnspecified, + check: AccessTypeEverybody, + isSubSet: false, + }, + "unspecified !< unspecified": { + superSet: AccessTypeUnspecified, + check: AccessTypeUnspecified, + isSubSet: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + subset := spec.check.IsSubset(spec.superSet) + require.Equal(t, spec.isSubSet, subset) + }) + } +} + +func TestContractCodeHistoryEntryValidation(t *testing.T) { + specs := map[string]struct { + src ContractCodeHistoryEntry + expErr bool + }{ + "all good": { + src: ContractCodeHistoryEntryFixture(), + }, + "unknown operation": { + src: ContractCodeHistoryEntryFixture(func(entry *ContractCodeHistoryEntry) { + entry.Operation = 0 + }), + expErr: true, + }, + "empty code id": { + src: ContractCodeHistoryEntryFixture(func(entry *ContractCodeHistoryEntry) { + entry.CodeID = 0 + }), + expErr: true, + }, + "empty updated": { + src: ContractCodeHistoryEntryFixture(func(entry *ContractCodeHistoryEntry) { + entry.Updated = nil + }), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} diff --git a/x/wasm/types/validation.go b/x/wasm/types/validation.go new file mode 100644 index 00000000..526a6bf5 --- /dev/null +++ b/x/wasm/types/validation.go @@ -0,0 +1,79 @@ +package types + +import ( + "fmt" + "net/url" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/docker/distribution/reference" +) + +// MaxSaltSize is the longest salt that can be used when instantiating a contract +const MaxSaltSize = 64 + +var ( + // MaxLabelSize is the longest label that can be used when instantiating a contract + MaxLabelSize = 128 // extension point for chains to customize via compile flag. + + // MaxWasmSize is the largest a compiled contract code can be when storing code on chain + MaxWasmSize = 800 * 1024 // extension point for chains to customize via compile flag. + + // MaxProposalWasmSize is the largest a gov proposal compiled contract code can be when storing code on chain + MaxProposalWasmSize = 3 * 1024 * 1024 // extension point for chains to customize via compile flag. +) + +func validateWasmCode(s []byte, maxSize int) error { + if len(s) == 0 { + return sdkerrors.Wrap(ErrEmpty, "is required") + } + if len(s) > maxSize { + return sdkerrors.Wrapf(ErrLimit, "cannot be longer than %d bytes", maxSize) + } + return nil +} + +// ValidateLabel ensure label constraints +func ValidateLabel(label string) error { + if label == "" { + return sdkerrors.Wrap(ErrEmpty, "is required") + } + if len(label) > MaxLabelSize { + return ErrLimit.Wrapf("cannot be longer than %d characters", MaxLabelSize) + } + return nil +} + +// ValidateSalt ensure salt constraints +func ValidateSalt(salt []byte) error { + switch n := len(salt); { + case n == 0: + return sdkerrors.Wrap(ErrEmpty, "is required") + case n > MaxSaltSize: + return ErrLimit.Wrapf("cannot be longer than %d characters", MaxSaltSize) + } + return nil +} + +// ValidateVerificationInfo ensure source, builder and checksum constraints +func ValidateVerificationInfo(source, builder string, codeHash []byte) error { + // if any set require others to be set + if len(source) != 0 || len(builder) != 0 || codeHash != nil { + if source == "" { + return fmt.Errorf("source is required") + } + if _, err := url.ParseRequestURI(source); err != nil { + return fmt.Errorf("source: %s", err) + } + if builder == "" { + return fmt.Errorf("builder is required") + } + if _, err := reference.ParseDockerRef(builder); err != nil { + return fmt.Errorf("builder: %s", err) + } + if codeHash == nil { + return fmt.Errorf("code hash is required") + } + // code hash checksum match validation is done in the keeper, ungzipping consumes gas + } + return nil +} diff --git a/x/wasm/types/wasmer_engine.go b/x/wasm/types/wasmer_engine.go new file mode 100644 index 00000000..9b3cf71a --- /dev/null +++ b/x/wasm/types/wasmer_engine.go @@ -0,0 +1,241 @@ +package types + +import ( + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" +) + +// DefaultMaxQueryStackSize maximum size of the stack of contract instances doing queries +const DefaultMaxQueryStackSize uint32 = 10 + +// WasmerEngine defines the WASM contract runtime engine. +type WasmerEngine interface { + // Create will compile the wasm code, and store the resulting pre-compile + // as well as the original code. Both can be referenced later via CodeID + // This must be done one time for given code, after which it can be + // instatitated many times, and each instance called many times. + // + // For example, the code for all ERC-20 contracts should be the same. + // This function stores the code for that contract only once, but it can + // be instantiated with custom inputs in the future. + Create(code wasmvm.WasmCode) (wasmvm.Checksum, error) + + // AnalyzeCode will statically analyze the code. + // Currently just reports if it exposes all IBC entry points. + AnalyzeCode(checksum wasmvm.Checksum) (*wasmvmtypes.AnalysisReport, error) + + // Instantiate will create a new contract based on the given codeID. + // We can set the initMsg (contract "genesis") here, and it then receives + // an account and address and can be invoked (Execute) many times. + // + // Storage should be set with a PrefixedKVStore that this code can safely access. + // + // Under the hood, we may recompile the wasm, use a cached native compile, or even use a cached instance + // for performance. + Instantiate( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + info wasmvmtypes.MessageInfo, + initMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) + + // Execute calls a given contract. Since the only difference between contracts with the same CodeID is the + // data in their local storage, and their address in the outside world, we need no ContractID here. + // (That is a detail for the external, sdk-facing, side). + // + // The caller is responsible for passing the correct `store` (which must have been initialized exactly once), + // and setting the env with relevant info on this instance (address, balance, etc) + Execute( + code wasmvm.Checksum, + env wasmvmtypes.Env, + info wasmvmtypes.MessageInfo, + executeMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) + + // Query allows a client to execute a contract-specific query. If the result is not empty, it should be + // valid json-encoded data to return to the client. + // The meaning of path and data can be determined by the code. Path is the suffix of the abci.QueryRequest.Path + Query( + code wasmvm.Checksum, + env wasmvmtypes.Env, + queryMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) ([]byte, uint64, error) + + // Migrate will migrate an existing contract to a new code binary. + // This takes storage of the data from the original contract and the CodeID of the new contract that should + // replace it. This allows it to run a migration step if needed, or return an error if unable to migrate + // the given data. + // + // MigrateMsg has some data on how to perform the migration. + Migrate( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + migrateMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) + + // Sudo runs an existing contract in read/write mode (like Execute), but is never exposed to external callers + // (either transactions or government proposals), but can only be called by other native Go modules directly. + // + // This allows a contract to expose custom "super user" functions or priviledged operations that can be + // deeply integrated with native modules. + Sudo( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + sudoMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) + + // Reply is called on the original dispatching contract after running a submessage + Reply( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + reply wasmvmtypes.Reply, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) + + // GetCode will load the original wasm code for the given code id. + // This will only succeed if that code id was previously returned from + // a call to Create. + // + // This can be used so that the (short) code id (hash) is stored in the iavl tree + // and the larger binary blobs (wasm and pre-compiles) are all managed by the + // rust library + GetCode(code wasmvm.Checksum) (wasmvm.WasmCode, error) + + // Cleanup should be called when no longer using this to free resources on the rust-side + Cleanup() + + // IBCChannelOpen is available on IBC-enabled contracts and is a hook to call into + // during the handshake pahse + IBCChannelOpen( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + channel wasmvmtypes.IBCChannelOpenMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBC3ChannelOpenResponse, uint64, error) + + // IBCChannelConnect is available on IBC-enabled contracts and is a hook to call into + // during the handshake pahse + IBCChannelConnect( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + channel wasmvmtypes.IBCChannelConnectMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + // IBCChannelClose is available on IBC-enabled contracts and is a hook to call into + // at the end of the channel lifetime + IBCChannelClose( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + channel wasmvmtypes.IBCChannelCloseMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + // IBCPacketReceive is available on IBC-enabled contracts and is called when an incoming + // packet is received on a channel belonging to this contract + IBCPacketReceive( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + packet wasmvmtypes.IBCPacketReceiveMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCReceiveResult, uint64, error) + + // IBCPacketAck is available on IBC-enabled contracts and is called when an + // the response for an outgoing packet (previously sent by this contract) + // is received + IBCPacketAck( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + ack wasmvmtypes.IBCPacketAckMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + // IBCPacketTimeout is available on IBC-enabled contracts and is called when an + // outgoing packet (previously sent by this contract) will provably never be executed. + // Usually handled like ack returning an error + IBCPacketTimeout( + checksum wasmvm.Checksum, + env wasmvmtypes.Env, + packet wasmvmtypes.IBCPacketTimeoutMsg, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + deserCost wasmvmtypes.UFraction, + ) (*wasmvmtypes.IBCBasicResponse, uint64, error) + + // Pin pins a code to an in-memory cache, such that is + // always loaded quickly when executed. + // Pin is idempotent. + Pin(checksum wasmvm.Checksum) error + + // Unpin removes the guarantee of a contract to be pinned (see Pin). + // After calling this, the code may or may not remain in memory depending on + // the implementor's choice. + // Unpin is idempotent. + Unpin(checksum wasmvm.Checksum) error + + // GetMetrics some internal metrics for monitoring purposes. + GetMetrics() (*wasmvmtypes.Metrics, error) +}