feat: Port x/wasm module #92

Closed
aleem1314 wants to merge 3 commits from aleem/25-wasmd into main
166 changed files with 64034 additions and 0 deletions

109
proto/wasm/v1/authz.proto Normal file
View File

@ -0,0 +1,109 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "google/protobuf/any.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
option (gogoproto.goproto_getters_all) = false;
// ContractExecutionAuthorization defines authorization for wasm execute.
// Since: wasmd 0.30
message ContractExecutionAuthorization {
option (cosmos_proto.implements_interface) = "Authorization";
// Grants for contract executions
repeated ContractGrant grants = 1 [ (gogoproto.nullable) = false ];
}
// ContractMigrationAuthorization defines authorization for wasm contract
// migration. Since: wasmd 0.30
message ContractMigrationAuthorization {
option (cosmos_proto.implements_interface) = "Authorization";
// Grants for contract migrations
repeated ContractGrant grants = 1 [ (gogoproto.nullable) = false ];
}
// ContractGrant a granted permission for a single contract
// Since: wasmd 0.30
message ContractGrant {
// Contract is the bech32 address of the smart contract
string contract = 1;
// Limit defines execution limits that are enforced and updated when the grant
// is applied. When the limit lapsed the grant is removed.
google.protobuf.Any limit = 2
[ (cosmos_proto.accepts_interface) = "ContractAuthzLimitX" ];
// Filter 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.
google.protobuf.Any filter = 3
[ (cosmos_proto.accepts_interface) = "ContractAuthzFilterX" ];
}
// MaxCallsLimit limited number of calls to the contract. No funds transferable.
// Since: wasmd 0.30
message MaxCallsLimit {
option (cosmos_proto.implements_interface) = "ContractAuthzLimitX";
// Remaining number that is decremented on each execution
uint64 remaining = 1;
}
// MaxFundsLimit defines the maximal amounts that can be sent to the contract.
// Since: wasmd 0.30
message MaxFundsLimit {
option (cosmos_proto.implements_interface) = "ContractAuthzLimitX";
// Amounts is the maximal amount of tokens transferable to the contract.
repeated cosmos.base.v1beta1.Coin amounts = 1 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
// CombinedLimit defines the maximal amounts that can be sent to a contract and
// the maximal number of calls executable. Both need to remain >0 to be valid.
// Since: wasmd 0.30
message CombinedLimit {
option (cosmos_proto.implements_interface) = "ContractAuthzLimitX";
// Remaining number that is decremented on each execution
uint64 calls_remaining = 1;
// Amounts is the maximal amount of tokens transferable to the contract.
repeated cosmos.base.v1beta1.Coin amounts = 2 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
// AllowAllMessagesFilter is a wildcard to allow any type of contract payload
// message.
// Since: wasmd 0.30
message AllowAllMessagesFilter {
option (cosmos_proto.implements_interface) = "ContractAuthzFilterX";
}
// AcceptedMessageKeysFilter accept only the specific contract message keys in
// the json object to be executed.
// Since: wasmd 0.30
message AcceptedMessageKeysFilter {
option (cosmos_proto.implements_interface) = "ContractAuthzFilterX";
// Messages is the list of unique keys
repeated string keys = 1;
}
// AcceptedMessagesFilter accept only the specific raw contract messages to be
// executed.
// Since: wasmd 0.30
message AcceptedMessagesFilter {
option (cosmos_proto.implements_interface) = "ContractAuthzFilterX";
// Messages is the list of raw contract messages
repeated bytes messages = 1 [ (gogoproto.casttype) = "RawContractMessage" ];
}

View File

@ -0,0 +1,46 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "gogoproto/gogo.proto";
import "wasm/v1/types.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
// GenesisState - genesis state of x/wasm
message GenesisState {
Params params = 1 [ (gogoproto.nullable) = false ];
repeated Code codes = 2
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "codes,omitempty" ];
repeated Contract contracts = 3 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "contracts,omitempty"
];
repeated Sequence sequences = 4 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "sequences,omitempty"
];
}
// Code struct encompasses CodeInfo and CodeBytes
message Code {
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
CodeInfo code_info = 2 [ (gogoproto.nullable) = false ];
bytes code_bytes = 3;
// Pinned to wasmvm cache
bool pinned = 4;
}
// Contract struct encompasses ContractAddress, ContractInfo, and ContractState
message Contract {
string contract_address = 1;
ContractInfo contract_info = 2 [ (gogoproto.nullable) = false ];
repeated Model contract_state = 3 [ (gogoproto.nullable) = false ];
repeated ContractCodeHistoryEntry contract_code_history = 4
[ (gogoproto.nullable) = false ];
}
// Sequence key and value of an id generation counter
message Sequence {
bytes id_key = 1 [ (gogoproto.customname) = "IDKey" ];
uint64 value = 2;
}

31
proto/wasm/v1/ibc.proto Normal file
View File

@ -0,0 +1,31 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "gogoproto/gogo.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
option (gogoproto.goproto_getters_all) = false;
// MsgIBCSend
message MsgIBCSend {
// the channel by which the packet will be sent
string channel = 2 [ (gogoproto.moretags) = "yaml:\"source_channel\"" ];
// Timeout height relative to the current block height.
// The timeout is disabled when set to 0.
uint64 timeout_height = 4
[ (gogoproto.moretags) = "yaml:\"timeout_height\"" ];
// Timeout timestamp (in nanoseconds) relative to the current block timestamp.
// The timeout is disabled when set to 0.
uint64 timeout_timestamp = 5
[ (gogoproto.moretags) = "yaml:\"timeout_timestamp\"" ];
// Data is the payload to transfer. We must not make assumption what format or
// content is in here.
bytes data = 6;
}
// MsgIBCCloseChannel port and channel need to be owned by the contract
message MsgIBCCloseChannel {
string channel = 2 [ (gogoproto.moretags) = "yaml:\"source_channel\"" ];
}

View File

@ -0,0 +1,272 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "wasm/v1/types.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
option (gogoproto.goproto_stringer_all) = false;
option (gogoproto.goproto_getters_all) = false;
option (gogoproto.equal_all) = true;
// StoreCodeProposal gov proposal content type to submit WASM code to the system
message StoreCodeProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// RunAs is the address that is passed to the contract's environment as sender
string run_as = 3;
// WASMByteCode can be raw or gzip compressed
bytes wasm_byte_code = 4 [ (gogoproto.customname) = "WASMByteCode" ];
// Used in v1beta1
reserved 5, 6;
// InstantiatePermission to apply on contract creation, optional
AccessConfig instantiate_permission = 7;
// UnpinCode code on upload, optional
bool unpin_code = 8;
// Source is the URL where the code is hosted
string source = 9;
// Builder is the docker image used to build the code deterministically, used
// for smart contract verification
string builder = 10;
// CodeHash is the SHA256 sum of the code outputted by builder, used for smart
// contract verification
bytes code_hash = 11;
}
// InstantiateContractProposal gov proposal content type to instantiate a
// contract.
message InstantiateContractProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// RunAs is the address that is passed to the contract's environment as sender
string run_as = 3;
// Admin is an optional address that can execute migrations
string admin = 4;
// CodeID is the reference to the stored WASM code
uint64 code_id = 5 [ (gogoproto.customname) = "CodeID" ];
// Label is optional metadata to be stored with a constract instance.
string label = 6;
// Msg json encoded message to be passed to the contract on instantiation
bytes msg = 7 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on instantiation
repeated cosmos.base.v1beta1.Coin funds = 8 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
// InstantiateContract2Proposal gov proposal content type to instantiate
// contract 2
message InstantiateContract2Proposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// RunAs is the address that is passed to the contract's enviroment as sender
string run_as = 3;
// Admin is an optional address that can execute migrations
string admin = 4;
// CodeID is the reference to the stored WASM code
uint64 code_id = 5 [ (gogoproto.customname) = "CodeID" ];
// Label is optional metadata to be stored with a constract instance.
string label = 6;
// Msg json encode message to be passed to the contract on instantiation
bytes msg = 7 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on instantiation
repeated cosmos.base.v1beta1.Coin funds = 8 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
// Salt is an arbitrary value provided by the sender. Size can be 1 to 64.
bytes salt = 9;
// FixMsg include the msg value into the hash for the predictable address.
// Default is false
bool fix_msg = 10;
}
// MigrateContractProposal gov proposal content type to migrate a contract.
message MigrateContractProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// Note: skipping 3 as this was previously used for unneeded run_as
// Contract is the address of the smart contract
string contract = 4;
// CodeID references the new WASM code
uint64 code_id = 5 [ (gogoproto.customname) = "CodeID" ];
// Msg json encoded message to be passed to the contract on migration
bytes msg = 6 [ (gogoproto.casttype) = "RawContractMessage" ];
}
// SudoContractProposal gov proposal content type to call sudo on a contract.
message SudoContractProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// Contract is the address of the smart contract
string contract = 3;
// Msg json encoded message to be passed to the contract as sudo
bytes msg = 4 [ (gogoproto.casttype) = "RawContractMessage" ];
}
// ExecuteContractProposal gov proposal content type to call execute on a
// contract.
message ExecuteContractProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// RunAs is the address that is passed to the contract's environment as sender
string run_as = 3;
// Contract is the address of the smart contract
string contract = 4;
// Msg json encoded message to be passed to the contract as execute
bytes msg = 5 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on instantiation
repeated cosmos.base.v1beta1.Coin funds = 6 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
// UpdateAdminProposal gov proposal content type to set an admin for a contract.
message UpdateAdminProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// NewAdmin address to be set
string new_admin = 3 [ (gogoproto.moretags) = "yaml:\"new_admin\"" ];
// Contract is the address of the smart contract
string contract = 4;
}
// ClearAdminProposal gov proposal content type to clear the admin of a
// contract.
message ClearAdminProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// Contract is the address of the smart contract
string contract = 3;
}
// PinCodesProposal gov proposal content type to pin a set of code ids in the
// wasmvm cache.
message PinCodesProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
// Description is a human readable text
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
// CodeIDs references the new WASM codes
repeated uint64 code_ids = 3 [
(gogoproto.customname) = "CodeIDs",
(gogoproto.moretags) = "yaml:\"code_ids\""
];
}
// UnpinCodesProposal gov proposal content type to unpin a set of code ids in
// the wasmvm cache.
message UnpinCodesProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
// Description is a human readable text
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
// CodeIDs references the WASM codes
repeated uint64 code_ids = 3 [
(gogoproto.customname) = "CodeIDs",
(gogoproto.moretags) = "yaml:\"code_ids\""
];
}
// AccessConfigUpdate contains the code id and the access config to be
// applied.
message AccessConfigUpdate {
// CodeID is the reference to the stored WASM code to be updated
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
// InstantiatePermission to apply to the set of code ids
AccessConfig instantiate_permission = 2 [ (gogoproto.nullable) = false ];
}
// UpdateInstantiateConfigProposal gov proposal content type to update
// instantiate config to a set of code ids.
message UpdateInstantiateConfigProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
// Description is a human readable text
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
// AccessConfigUpdate contains the list of code ids and the access config
// to be applied.
repeated AccessConfigUpdate access_config_updates = 3
[ (gogoproto.nullable) = false ];
}
// StoreAndInstantiateContractProposal gov proposal content type to store
// and instantiate the contract.
message StoreAndInstantiateContractProposal {
option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content";
// Title is a short summary
string title = 1;
// Description is a human readable text
string description = 2;
// RunAs is the address that is passed to the contract's environment as sender
string run_as = 3;
// WASMByteCode can be raw or gzip compressed
bytes wasm_byte_code = 4 [ (gogoproto.customname) = "WASMByteCode" ];
// InstantiatePermission to apply on contract creation, optional
AccessConfig instantiate_permission = 5;
// UnpinCode code on upload, optional
bool unpin_code = 6;
// Admin is an optional address that can execute migrations
string admin = 7;
// Label is optional metadata to be stored with a constract instance.
string label = 8;
// Msg json encoded message to be passed to the contract on instantiation
bytes msg = 9 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on instantiation
repeated cosmos.base.v1beta1.Coin funds = 10 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
// Source is the URL where the code is hosted
string source = 11;
// Builder is the docker image used to build the code deterministically, used
// for smart contract verification
string builder = 12;
// CodeHash is the SHA256 sum of the code outputted by builder, used for smart
// contract verification
bytes code_hash = 13;
}

263
proto/wasm/v1/query.proto Normal file
View File

@ -0,0 +1,263 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "gogoproto/gogo.proto";
import "wasm/v1/types.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
option (gogoproto.goproto_getters_all) = false;
option (gogoproto.equal_all) = false;
// Query provides defines the gRPC querier service
service Query {
// ContractInfo gets the contract meta data
rpc ContractInfo(QueryContractInfoRequest)
returns (QueryContractInfoResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/contract/{address}";
}
// ContractHistory gets the contract code history
rpc ContractHistory(QueryContractHistoryRequest)
returns (QueryContractHistoryResponse) {
option (google.api.http).get =
"/cosmwasm/wasm/v1/contract/{address}/history";
}
// ContractsByCode lists all smart contracts for a code id
rpc ContractsByCode(QueryContractsByCodeRequest)
returns (QueryContractsByCodeResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/code/{code_id}/contracts";
}
// AllContractState gets all raw store data for a single contract
rpc AllContractState(QueryAllContractStateRequest)
returns (QueryAllContractStateResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/contract/{address}/state";
}
// RawContractState gets single key from the raw store data of a contract
rpc RawContractState(QueryRawContractStateRequest)
returns (QueryRawContractStateResponse) {
option (google.api.http).get =
"/cosmwasm/wasm/v1/contract/{address}/raw/{query_data}";
}
// SmartContractState get smart query result from the contract
rpc SmartContractState(QuerySmartContractStateRequest)
returns (QuerySmartContractStateResponse) {
option (google.api.http).get =
"/cosmwasm/wasm/v1/contract/{address}/smart/{query_data}";
}
// Code gets the binary code and metadata for a singe wasm code
rpc Code(QueryCodeRequest) returns (QueryCodeResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/code/{code_id}";
}
// Codes gets the metadata for all stored wasm codes
rpc Codes(QueryCodesRequest) returns (QueryCodesResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/code";
}
// PinnedCodes gets the pinned code ids
rpc PinnedCodes(QueryPinnedCodesRequest) returns (QueryPinnedCodesResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/codes/pinned";
}
// Params gets the module params
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/codes/params";
}
// ContractsByCreator gets the contracts by creator
rpc ContractsByCreator(QueryContractsByCreatorRequest)
returns (QueryContractsByCreatorResponse) {
option (google.api.http).get =
"/cosmwasm/wasm/v1/contracts/creator/{creator_address}";
}
}
// QueryContractInfoRequest is the request type for the Query/ContractInfo RPC
// method
message QueryContractInfoRequest {
// address is the address of the contract to query
string address = 1;
}
// QueryContractInfoResponse is the response type for the Query/ContractInfo RPC
// method
message QueryContractInfoResponse {
option (gogoproto.equal) = true;
// address is the address of the contract
string address = 1;
ContractInfo contract_info = 2 [
(gogoproto.embed) = true,
(gogoproto.nullable) = false,
(gogoproto.jsontag) = ""
];
}
// QueryContractHistoryRequest is the request type for the Query/ContractHistory
// RPC method
message QueryContractHistoryRequest {
// address is the address of the contract to query
string address = 1;
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryContractHistoryResponse is the response type for the
// Query/ContractHistory RPC method
message QueryContractHistoryResponse {
repeated ContractCodeHistoryEntry entries = 1
[ (gogoproto.nullable) = false ];
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// QueryContractsByCodeRequest is the request type for the Query/ContractsByCode
// RPC method
message QueryContractsByCodeRequest {
uint64 code_id = 1; // grpc-gateway_out does not support Go style CodID
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryContractsByCodeResponse is the response type for the
// Query/ContractsByCode RPC method
message QueryContractsByCodeResponse {
// contracts are a set of contract addresses
repeated string contracts = 1;
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// QueryAllContractStateRequest is the request type for the
// Query/AllContractState RPC method
message QueryAllContractStateRequest {
// address is the address of the contract
string address = 1;
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryAllContractStateResponse is the response type for the
// Query/AllContractState RPC method
message QueryAllContractStateResponse {
repeated Model models = 1 [ (gogoproto.nullable) = false ];
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// QueryRawContractStateRequest is the request type for the
// Query/RawContractState RPC method
message QueryRawContractStateRequest {
// address is the address of the contract
string address = 1;
bytes query_data = 2;
}
// QueryRawContractStateResponse is the response type for the
// Query/RawContractState RPC method
message QueryRawContractStateResponse {
// Data contains the raw store data
bytes data = 1;
}
// QuerySmartContractStateRequest is the request type for the
// Query/SmartContractState RPC method
message QuerySmartContractStateRequest {
// address is the address of the contract
string address = 1;
// QueryData contains the query data passed to the contract
bytes query_data = 2 [ (gogoproto.casttype) = "RawContractMessage" ];
}
// QuerySmartContractStateResponse is the response type for the
// Query/SmartContractState RPC method
message QuerySmartContractStateResponse {
// Data contains the json data returned from the smart contract
bytes data = 1 [ (gogoproto.casttype) = "RawContractMessage" ];
}
// QueryCodeRequest is the request type for the Query/Code RPC method
message QueryCodeRequest {
uint64 code_id = 1; // grpc-gateway_out does not support Go style CodID
}
// CodeInfoResponse contains code meta data from CodeInfo
message CodeInfoResponse {
option (gogoproto.equal) = true;
uint64 code_id = 1 [
(gogoproto.customname) = "CodeID",
(gogoproto.jsontag) = "id"
]; // id for legacy support
string creator = 2;
bytes data_hash = 3
[ (gogoproto.casttype) =
"github.com/tendermint/tendermint/libs/bytes.HexBytes" ];
// Used in v1beta1
reserved 4, 5;
AccessConfig instantiate_permission = 6 [ (gogoproto.nullable) = false ];
}
// QueryCodeResponse is the response type for the Query/Code RPC method
message QueryCodeResponse {
option (gogoproto.equal) = true;
CodeInfoResponse code_info = 1
[ (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
bytes data = 2 [ (gogoproto.jsontag) = "data" ];
}
// QueryCodesRequest is the request type for the Query/Codes RPC method
message QueryCodesRequest {
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}
// QueryCodesResponse is the response type for the Query/Codes RPC method
message QueryCodesResponse {
repeated CodeInfoResponse code_infos = 1 [ (gogoproto.nullable) = false ];
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// QueryPinnedCodesRequest is the request type for the Query/PinnedCodes
// RPC method
message QueryPinnedCodesRequest {
// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryPinnedCodesResponse is the response type for the
// Query/PinnedCodes RPC method
message QueryPinnedCodesResponse {
repeated uint64 code_ids = 1
[ (gogoproto.nullable) = false, (gogoproto.customname) = "CodeIDs" ];
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// QueryParamsRequest is the request type for the Query/Params RPC method.
message QueryParamsRequest {}
// QueryParamsResponse is the response type for the Query/Params RPC method.
message QueryParamsResponse {
// params defines the parameters of the module.
Params params = 1 [ (gogoproto.nullable) = false ];
}
// QueryContractsByCreatorRequest is the request type for the
// Query/ContractsByCreator RPC method.
message QueryContractsByCreatorRequest {
// CreatorAddress is the address of contract creator
string creator_address = 1;
// Pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryContractsByCreatorResponse is the response type for the
// Query/ContractsByCreator RPC method.
message QueryContractsByCreatorResponse {
// ContractAddresses result set
repeated string contract_addresses = 1;
// Pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

192
proto/wasm/v1/tx.proto Normal file
View File

@ -0,0 +1,192 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";
import "wasm/v1/types.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
option (gogoproto.goproto_getters_all) = false;
// Msg defines the wasm Msg service.
service Msg {
// StoreCode to submit Wasm code to the system
rpc StoreCode(MsgStoreCode) returns (MsgStoreCodeResponse);
// InstantiateContract creates a new smart contract instance for the given
// code id.
rpc InstantiateContract(MsgInstantiateContract)
returns (MsgInstantiateContractResponse);
// InstantiateContract2 creates a new smart contract instance for the given
// code id with a predictable address
rpc InstantiateContract2(MsgInstantiateContract2)
returns (MsgInstantiateContract2Response);
// Execute submits the given message data to a smart contract
rpc ExecuteContract(MsgExecuteContract) returns (MsgExecuteContractResponse);
// Migrate runs a code upgrade/ downgrade for a smart contract
rpc MigrateContract(MsgMigrateContract) returns (MsgMigrateContractResponse);
// UpdateAdmin sets a new admin for a smart contract
rpc UpdateAdmin(MsgUpdateAdmin) returns (MsgUpdateAdminResponse);
// ClearAdmin removes any admin stored for a smart contract
rpc ClearAdmin(MsgClearAdmin) returns (MsgClearAdminResponse);
// UpdateInstantiateConfig updates instantiate config for a smart contract
rpc UpdateInstantiateConfig(MsgUpdateInstantiateConfig)
returns (MsgUpdateInstantiateConfigResponse);
}
// MsgStoreCode submit Wasm code to the system
message MsgStoreCode {
// Sender is the that actor that signed the messages
string sender = 1;
// WASMByteCode can be raw or gzip compressed
bytes wasm_byte_code = 2 [ (gogoproto.customname) = "WASMByteCode" ];
// Used in v1beta1
reserved 3, 4;
// InstantiatePermission access control to apply on contract creation,
// optional
AccessConfig instantiate_permission = 5;
}
// MsgStoreCodeResponse returns store result data.
message MsgStoreCodeResponse {
// CodeID is the reference to the stored WASM code
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
// Checksum is the sha256 hash of the stored code
bytes checksum = 2;
}
// MsgInstantiateContract create a new smart contract instance for the given
// code id.
message MsgInstantiateContract {
// Sender is the that actor that signed the messages
string sender = 1;
// Admin is an optional address that can execute migrations
string admin = 2;
// CodeID is the reference to the stored WASM code
uint64 code_id = 3 [ (gogoproto.customname) = "CodeID" ];
// Label is optional metadata to be stored with a contract instance.
string label = 4;
// Msg json encoded message to be passed to the contract on instantiation
bytes msg = 5 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on instantiation
repeated cosmos.base.v1beta1.Coin funds = 6 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
// MsgInstantiateContract2 create a new smart contract instance for the given
// code id with a predicable address.
message MsgInstantiateContract2 {
// Sender is the that actor that signed the messages
string sender = 1;
// Admin is an optional address that can execute migrations
string admin = 2;
// CodeID is the reference to the stored WASM code
uint64 code_id = 3 [ (gogoproto.customname) = "CodeID" ];
// Label is optional metadata to be stored with a contract instance.
string label = 4;
// Msg json encoded message to be passed to the contract on instantiation
bytes msg = 5 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on instantiation
repeated cosmos.base.v1beta1.Coin funds = 6 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
// Salt is an arbitrary value provided by the sender. Size can be 1 to 64.
bytes salt = 7;
// FixMsg include the msg value into the hash for the predictable address.
// Default is false
bool fix_msg = 8;
}
// MsgInstantiateContractResponse return instantiation result data
message MsgInstantiateContractResponse {
// Address is the bech32 address of the new contract instance.
string address = 1;
// Data contains bytes to returned from the contract
bytes data = 2;
}
// MsgInstantiateContract2Response return instantiation result data
message MsgInstantiateContract2Response {
// Address is the bech32 address of the new contract instance.
string address = 1;
// Data contains bytes to returned from the contract
bytes data = 2;
}
// MsgExecuteContract submits the given message data to a smart contract
message MsgExecuteContract {
// Sender is the that actor that signed the messages
string sender = 1;
// Contract is the address of the smart contract
string contract = 2;
// Msg json encoded message to be passed to the contract
bytes msg = 3 [ (gogoproto.casttype) = "RawContractMessage" ];
// Funds coins that are transferred to the contract on execution
repeated cosmos.base.v1beta1.Coin funds = 5 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
// MsgExecuteContractResponse returns execution result data.
message MsgExecuteContractResponse {
// Data contains bytes to returned from the contract
bytes data = 1;
}
// MsgMigrateContract runs a code upgrade/ downgrade for a smart contract
message MsgMigrateContract {
// Sender is the that actor that signed the messages
string sender = 1;
// Contract is the address of the smart contract
string contract = 2;
// CodeID references the new WASM code
uint64 code_id = 3 [ (gogoproto.customname) = "CodeID" ];
// Msg json encoded message to be passed to the contract on migration
bytes msg = 4 [ (gogoproto.casttype) = "RawContractMessage" ];
}
// MsgMigrateContractResponse returns contract migration result data.
message MsgMigrateContractResponse {
// Data contains same raw bytes returned as data from the wasm contract.
// (May be empty)
bytes data = 1;
}
// MsgUpdateAdmin sets a new admin for a smart contract
message MsgUpdateAdmin {
// Sender is the that actor that signed the messages
string sender = 1;
// NewAdmin address to be set
string new_admin = 2;
// Contract is the address of the smart contract
string contract = 3;
}
// MsgUpdateAdminResponse returns empty data
message MsgUpdateAdminResponse {}
// MsgClearAdmin removes any admin stored for a smart contract
message MsgClearAdmin {
// Sender is the actor that signed the messages
string sender = 1;
// Contract is the address of the smart contract
string contract = 3;
}
// MsgClearAdminResponse returns empty data
message MsgClearAdminResponse {}
// MsgUpdateInstantiateConfig updates instantiate config for a smart contract
message MsgUpdateInstantiateConfig {
// Sender is the that actor that signed the messages
string sender = 1;
// CodeID references the stored WASM code
uint64 code_id = 2 [ (gogoproto.customname) = "CodeID" ];
// NewInstantiatePermission is the new access control
AccessConfig new_instantiate_permission = 3;
}
// MsgUpdateInstantiateConfigResponse returns empty data
message MsgUpdateInstantiateConfigResponse {}

144
proto/wasm/v1/types.proto Normal file
View File

@ -0,0 +1,144 @@
syntax = "proto3";
package cosmwasm.wasm.v1;
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
option go_package = "github.com/cerc-io/laconicd/x/wasm/types";
option (gogoproto.goproto_getters_all) = false;
option (gogoproto.equal_all) = true;
// AccessType permission types
enum AccessType {
option (gogoproto.goproto_enum_prefix) = false;
option (gogoproto.goproto_enum_stringer) = false;
// AccessTypeUnspecified placeholder for empty value
ACCESS_TYPE_UNSPECIFIED = 0
[ (gogoproto.enumvalue_customname) = "AccessTypeUnspecified" ];
// AccessTypeNobody forbidden
ACCESS_TYPE_NOBODY = 1
[ (gogoproto.enumvalue_customname) = "AccessTypeNobody" ];
// AccessTypeOnlyAddress restricted to a single address
// Deprecated: use AccessTypeAnyOfAddresses instead
ACCESS_TYPE_ONLY_ADDRESS = 2
[ (gogoproto.enumvalue_customname) = "AccessTypeOnlyAddress" ];
// AccessTypeEverybody unrestricted
ACCESS_TYPE_EVERYBODY = 3
[ (gogoproto.enumvalue_customname) = "AccessTypeEverybody" ];
// AccessTypeAnyOfAddresses allow any of the addresses
ACCESS_TYPE_ANY_OF_ADDRESSES = 4
[ (gogoproto.enumvalue_customname) = "AccessTypeAnyOfAddresses" ];
}
// AccessTypeParam
message AccessTypeParam {
option (gogoproto.goproto_stringer) = true;
AccessType value = 1 [ (gogoproto.moretags) = "yaml:\"value\"" ];
}
// AccessConfig access control type.
message AccessConfig {
option (gogoproto.goproto_stringer) = true;
AccessType permission = 1 [ (gogoproto.moretags) = "yaml:\"permission\"" ];
// Address
// Deprecated: replaced by addresses
string address = 2 [ (gogoproto.moretags) = "yaml:\"address\"" ];
repeated string addresses = 3 [ (gogoproto.moretags) = "yaml:\"addresses\"" ];
}
// Params defines the set of wasm parameters.
message Params {
option (gogoproto.goproto_stringer) = false;
AccessConfig code_upload_access = 1 [
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"code_upload_access\""
];
AccessType instantiate_default_permission = 2
[ (gogoproto.moretags) = "yaml:\"instantiate_default_permission\"" ];
}
// CodeInfo is data for the uploaded contract WASM code
message CodeInfo {
// CodeHash is the unique identifier created by wasmvm
bytes code_hash = 1;
// Creator address who initially stored the code
string creator = 2;
// Used in v1beta1
reserved 3, 4;
// InstantiateConfig access control to apply on contract creation, optional
AccessConfig instantiate_config = 5 [ (gogoproto.nullable) = false ];
}
// ContractInfo stores a WASM contract instance
message ContractInfo {
option (gogoproto.equal) = true;
// CodeID is the reference to the stored Wasm code
uint64 code_id = 1 [ (gogoproto.customname) = "CodeID" ];
// Creator address who initially instantiated the contract
string creator = 2;
// Admin is an optional address that can execute migrations
string admin = 3;
// Label is optional metadata to be stored with a contract instance.
string label = 4;
// Created Tx position when the contract was instantiated.
AbsoluteTxPosition created = 5;
string ibc_port_id = 6 [ (gogoproto.customname) = "IBCPortID" ];
// Extension is an extension point to store custom metadata within the
// persistence model.
google.protobuf.Any extension = 7
[ (cosmos_proto.accepts_interface) = "ContractInfoExtension" ];
}
// ContractCodeHistoryOperationType actions that caused a code change
enum ContractCodeHistoryOperationType {
option (gogoproto.goproto_enum_prefix) = false;
// ContractCodeHistoryOperationTypeUnspecified placeholder for empty value
CONTRACT_CODE_HISTORY_OPERATION_TYPE_UNSPECIFIED = 0
[ (gogoproto.enumvalue_customname) =
"ContractCodeHistoryOperationTypeUnspecified" ];
// ContractCodeHistoryOperationTypeInit on chain contract instantiation
CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT = 1
[ (gogoproto.enumvalue_customname) =
"ContractCodeHistoryOperationTypeInit" ];
// ContractCodeHistoryOperationTypeMigrate code migration
CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE = 2
[ (gogoproto.enumvalue_customname) =
"ContractCodeHistoryOperationTypeMigrate" ];
// ContractCodeHistoryOperationTypeGenesis based on genesis data
CONTRACT_CODE_HISTORY_OPERATION_TYPE_GENESIS = 3
[ (gogoproto.enumvalue_customname) =
"ContractCodeHistoryOperationTypeGenesis" ];
}
// ContractCodeHistoryEntry metadata to a contract.
message ContractCodeHistoryEntry {
ContractCodeHistoryOperationType operation = 1;
// CodeID is the reference to the stored WASM code
uint64 code_id = 2 [ (gogoproto.customname) = "CodeID" ];
// Updated Tx position when the operation was executed.
AbsoluteTxPosition updated = 3;
bytes msg = 4 [ (gogoproto.casttype) = "RawContractMessage" ];
}
// AbsoluteTxPosition is a unique transaction position that allows for global
// ordering of transactions.
message AbsoluteTxPosition {
// BlockHeight is the block the contract was created at
uint64 block_height = 1;
// TxIndex is a monotonic counter within the block (actual transaction index,
// or gas consumed)
uint64 tx_index = 2;
}
// Model is a struct that holds a KV pair
message Model {
// hex-encode key to read it better (this is often ascii)
bytes key = 1 [ (gogoproto.casttype) =
"github.com/tendermint/tendermint/libs/bytes.HexBytes" ];
// base64-encode raw value
bytes value = 2;
}

205
x/wasm/Governance.md Normal file
View File

@ -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 <proposal-json-file> --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

137
x/wasm/IBC.md Normal file
View File

@ -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.<contract address>`,
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-<bech32 address>` 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.
```

219
x/wasm/README.md Normal file
View File

@ -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

133
x/wasm/alias.go Normal file
View File

@ -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
)

834
x/wasm/client/cli/gov_tx.go Normal file
View File

@ -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"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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 := govv1beta1.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
}
github-code-scanning[bot] commented 2023-02-28 09:37:25 +00:00 (Migrated from github.com)
Review

Variable $X is likely modified and later used on error. In some cases this could result in panics due to a nil dereference

Variable proposalTitle is likely modified and later used on error. In some cases this could result in panics due to a nil dereference

Show more details

## Variable `$X` is likely modified and later used on error. In some cases this could result in panics due to a nil dereference Variable `proposalTitle` is likely modified and later used on error. In some cases this could result in panics due to a nil dereference [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/612)
proposalDescr, err := cmd.Flags().GetString(cli.FlagDescription)
if err != nil {
return client.Context{}, proposalTitle, proposalDescr, nil, err
}
github-code-scanning[bot] commented 2023-02-28 09:37:25 +00:00 (Migrated from github.com)
Review

Variable $X is likely modified and later used on error. In some cases this could result in panics due to a nil dereference

Variable proposalDescr is likely modified and later used on error. In some cases this could result in panics due to a nil dereference

Show more details

## Variable `$X` is likely modified and later used on error. In some cases this could result in panics due to a nil dereference Variable `proposalDescr` is likely modified and later used on error. In some cases this could result in panics due to a nil dereference [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/613)
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
}
github-code-scanning[bot] commented 2023-02-28 09:37:25 +00:00 (Migrated from github.com)
Review

Variable $X is likely modified and later used on error. In some cases this could result in panics due to a nil dereference

Variable deposit is likely modified and later used on error. In some cases this could result in panics due to a nil dereference

Show more details

## Variable `$X` is likely modified and later used on error. In some cases this could result in panics due to a nil dereference Variable `deposit` is likely modified and later used on error. In some cases this could result in panics due to a nil dereference [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/614)
return clientCtx, proposalTitle, proposalDescr, deposit, nil
}

View File

@ -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)
})
}
}

163
x/wasm/client/cli/new_tx.go Normal file
View File

@ -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
}

674
x/wasm/client/cli/query.go Normal file
View File

@ -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
}

544
x/wasm/client/cli/tx.go Normal file
View File

@ -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 <grantee_addr> execution <contract_addr> --allow-all-messages --max-calls 1 --no-token-transfer --expiration 1667979596
$ %s tx grant <grantee_addr> execution <contract_addr> --allow-all-messages --max-funds 100000uwasm --expiration 1667979596
$ %s tx grant <grantee_addr> execution <contract_addr> --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
}

View File

@ -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)
})
}
}

View File

@ -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),
}

View File

@ -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())
})
}
}

547
x/wasm/client/rest/gov.go Normal file
View File

@ -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")
},
}
}

View File

@ -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)
}
}

270
x/wasm/client/rest/query.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}

149
x/wasm/client/rest/tx.go Normal file
View File

@ -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)
}
}

34
x/wasm/common_test.go Normal file
View File

@ -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
}

96
x/wasm/genesis_test.go Normal file
View File

@ -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(),
})
}

77
x/wasm/handler.go Normal file
View File

@ -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
}

357
x/wasm/ibc.go Normal file
View File

@ -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
}

View File

@ -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)
})
}
}

124
x/wasm/ibc_reflect_test.go Normal file
View File

@ -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"`
}

82
x/wasm/ibc_test.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,2 @@
# testing package for ibc
Customized version of cosmos-sdk x/ibc/testing

594
x/wasm/ibctesting/chain.go Normal file
View File

@ -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)
}

View File

@ -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)
}
github-code-scanning[bot] commented 2023-02-28 09:56:28 +00:00 (Migrated from github.com)
Review

Iteration over map

Iteration over map may be a possible source of non-determinism

Show more details

## Iteration over map Iteration over map may be a possible source of non-determinism [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/624)
}
// 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)
}

View File

@ -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())
github-code-scanning[bot] commented 2023-02-28 09:37:24 +00:00 (Migrated from github.com)
Review

Should $X be modified when an error could be returned?

Should endpoint be modified when an error could be returned?

Show more details

## Should `$X` be modified when an error could be returned? Should `endpoint` be modified when an error could be returned? [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/602)
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())
github-code-scanning[bot] commented 2023-02-28 09:37:24 +00:00 (Migrated from github.com)
Review

Should $X be modified when an error could be returned?

Should endpoint be modified when an error could be returned?

Show more details

## Should `$X` be modified when an error could be returned? Should `endpoint` be modified when an error could be returned? [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/603)
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())
github-code-scanning[bot] commented 2023-02-28 09:37:24 +00:00 (Migrated from github.com)
Review

Should $X be modified when an error could be returned?

Should endpoint be modified when an error could be returned?

Show more details

## Should `$X` be modified when an error could be returned? Should `endpoint` be modified when an error could be returned? [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/604)
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())
github-code-scanning[bot] commented 2023-02-28 09:37:25 +00:00 (Migrated from github.com)
Review

Should $X be modified when an error could be returned?

Should endpoint be modified when an error could be returned?

Show more details

## Should `$X` be modified when an error could be returned? Should `endpoint` be modified when an error could be returned? [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/605)
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())
github-code-scanning[bot] commented 2023-02-28 09:37:25 +00:00 (Migrated from github.com)
Review

Should $X be modified when an error could be returned?

Should endpoint be modified when an error could be returned?

Show more details

## Should `$X` be modified when an error could be returned? Should `endpoint` be modified when an error could be returned? [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/606)
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
}

View File

@ -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")
}

View File

@ -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
}

113
x/wasm/ibctesting/path.go Normal file
View File

@ -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,
}
}

138
x/wasm/ibctesting/wasm.go Normal file
View File

@ -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)
}

41
x/wasm/ioutils/ioutil.go Normal file
View File

@ -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)
}

View File

@ -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()
}

43
x/wasm/ioutils/utils.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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...)
}

View File

@ -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"
}
}
]
`

96
x/wasm/keeper/ante.go Normal file
View File

@ -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)
}

189
x/wasm/keeper/ante_test.go Normal file
View File

@ -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
}
}

43
x/wasm/keeper/api.go Normal file
View File

@ -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,
}

View File

@ -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
}

View File

@ -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)
})
}
}

102
x/wasm/keeper/bench_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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)
}

View File

@ -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)
})
}
}

67
x/wasm/keeper/events.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
})
}
}

116
x/wasm/keeper/genesis.go Normal file
View File

@ -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
}

View File

@ -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}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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())
})
}
}

View File

@ -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
}

56
x/wasm/keeper/ibc.go Normal file
View File

@ -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)
}

82
x/wasm/keeper/ibc_test.go Normal file
View File

@ -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)
})
}
}

1188
x/wasm/keeper/keeper.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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")
}

2410
x/wasm/keeper/keeper_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,154 @@
package keeper
import (
"encoding/json"
"reflect"
github-code-scanning[bot] commented 2023-02-28 09:56:29 +00:00 (Migrated from github.com)
Review

Sensitive package import

Certain system packages contain functions which may be a possible source of non-determinism

Show more details

## Sensitive package import Certain system packages contain functions which may be a possible source of non-determinism [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/631)
"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() {
github-code-scanning[bot] commented 2023-02-28 09:56:28 +00:00 (Migrated from github.com)
Review

Impossible interface nil check

This value can never be nil, since it is a wrapped interface value.

Show more details

## Impossible interface nil check This value can never be nil, since it is a wrapped interface value. [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/634)
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
}

View File

@ -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"])
}
})
}
}

72
x/wasm/keeper/metrics.go Normal file
View File

@ -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.
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

256
x/wasm/keeper/msg_server.go Normal file
View File

@ -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
}

View File

@ -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)
}

170
x/wasm/keeper/options.go Normal file
View File

@ -0,0 +1,170 @@
package keeper
import (
"fmt"
"reflect"
github-code-scanning[bot] commented 2023-02-28 09:56:29 +00:00 (Migrated from github.com)
Review

Sensitive package import

Certain system packages contain functions which may be a possible source of non-determinism

Show more details

## Sensitive package import Certain system packages contain functions which may be a possible source of non-determinism [Show more details](https://github.com/cerc-io/laconicd/security/code-scanning/632)
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
}

View File

@ -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
}

View File

@ -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
}

File diff suppressed because it is too large Load Diff

346
x/wasm/keeper/querier.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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), &params)
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)
}

View File

@ -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)
})
}
}

View File

@ -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)
})
}
}

View File

@ -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")
}

203
x/wasm/keeper/relay.go Normal file
View File

@ -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
}

703
x/wasm/keeper/relay_test.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}

BIN
x/wasm/keeper/testdata/broken_crc.gzip vendored Normal file

Binary file not shown.

BIN
x/wasm/keeper/testdata/burner.wasm vendored Normal file

Binary file not shown.

23
x/wasm/keeper/testdata/download_releases.sh vendored Executable file
View File

@ -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

219
x/wasm/keeper/testdata/genesis.json vendored Normal file
View File

@ -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
}
}
}

Some files were not shown because too many files have changed in this diff Show More