wasmd/x/wasm/simulation/operations.go

556 lines
18 KiB
Go

package simulation
import (
"context"
"encoding/json"
"math/rand"
"os"
wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types"
errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/auth/tx"
"github.com/cosmos/cosmos-sdk/x/simulation"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
"github.com/CosmWasm/wasmd/x/wasm/keeper/testdata"
"github.com/CosmWasm/wasmd/x/wasm/types"
)
// Simulation operation weights constants
const (
OpWeightMsgStoreCode = "op_weight_msg_store_code"
OpWeightMsgInstantiateContract = "op_weight_msg_instantiate_contract"
OpWeightMsgExecuteContract = "op_weight_msg_execute_contract"
OpWeightMsgUpdateAdmin = "op_weight_msg_update_admin"
OpWeightMsgClearAdmin = "op_weight_msg_clear_admin"
OpWeightMsgMigrateContract = "op_weight_msg_migrate_contract"
OpReflectContractPath = "op_reflect_contract_path"
DefaultWeightMsgStoreCode int = 50
DefaultWeightMsgInstantiateContract int = 100
DefaultWeightMsgExecuteContract int = 100
DefaultWeightMsgUpdateAdmin int = 25
DefaultWeightMsgClearAdmin int = 10
DefaultWeightMsgMigrateContract int = 50
)
// WasmKeeper is a subset of the wasm keeper used by simulations
type WasmKeeper interface {
GetAuthority() string
GetParams(ctx context.Context) types.Params
IterateCodeInfos(ctx context.Context, cb func(uint64, types.CodeInfo) bool)
IterateContractInfo(ctx context.Context, cb func(sdk.AccAddress, types.ContractInfo) bool)
QuerySmart(ctx context.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error)
PeekAutoIncrementID(ctx context.Context, lastIDKey []byte) (uint64, error)
}
type BankKeeper interface {
simulation.BankKeeper
IsSendEnabledCoin(ctx context.Context, coin sdk.Coin) bool
}
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simtypes.AppParams,
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
) simulation.WeightedOperations {
var (
weightMsgStoreCode int
weightMsgInstantiateContract int
weightMsgExecuteContract int
weightMsgUpdateAdmin int
weightMsgClearAdmin int
weightMsgMigrateContract int
wasmContractPath string
)
appParams.GetOrGenerate(OpWeightMsgStoreCode, &weightMsgStoreCode, nil, func(_ *rand.Rand) {
weightMsgStoreCode = DefaultWeightMsgStoreCode
})
appParams.GetOrGenerate(OpWeightMsgInstantiateContract, &weightMsgInstantiateContract, nil, func(_ *rand.Rand) {
weightMsgInstantiateContract = DefaultWeightMsgInstantiateContract
})
appParams.GetOrGenerate(OpWeightMsgExecuteContract, &weightMsgInstantiateContract, nil, func(_ *rand.Rand) {
weightMsgExecuteContract = DefaultWeightMsgExecuteContract
})
appParams.GetOrGenerate(OpWeightMsgUpdateAdmin, &weightMsgUpdateAdmin, nil, func(_ *rand.Rand) {
weightMsgUpdateAdmin = DefaultWeightMsgUpdateAdmin
})
appParams.GetOrGenerate(OpWeightMsgClearAdmin, &weightMsgClearAdmin, nil, func(_ *rand.Rand) {
weightMsgClearAdmin = DefaultWeightMsgClearAdmin
})
appParams.GetOrGenerate(OpWeightMsgMigrateContract, &weightMsgMigrateContract, nil, func(_ *rand.Rand) {
weightMsgMigrateContract = DefaultWeightMsgMigrateContract
})
appParams.GetOrGenerate(OpReflectContractPath, &wasmContractPath, nil, func(_ *rand.Rand) {
wasmContractPath = ""
})
var wasmBz []byte
if wasmContractPath == "" {
wasmBz = testdata.MigrateReflectContractWasm()
} else {
var err error
wasmBz, err = os.ReadFile(wasmContractPath)
if err != nil {
panic(err)
}
}
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgStoreCode,
SimulateMsgStoreCode(ak, bk, wasmKeeper, wasmBz),
),
simulation.NewWeightedOperation(
weightMsgInstantiateContract,
SimulateMsgInstantiateContract(ak, bk, wasmKeeper, DefaultSimulationCodeIDSelector),
),
simulation.NewWeightedOperation(
weightMsgExecuteContract,
SimulateMsgExecuteContract(
ak,
bk,
wasmKeeper,
DefaultSimulationExecuteContractSelector,
DefaultSimulationExecuteSenderSelector,
DefaultSimulationExecutePayloader,
),
),
simulation.NewWeightedOperation(
weightMsgUpdateAdmin,
SimulateMsgUpdateAmin(
ak,
bk,
wasmKeeper,
DefaultSimulationUpdateAdminContractSelector,
),
),
simulation.NewWeightedOperation(
weightMsgClearAdmin,
SimulateMsgClearAdmin(
ak,
bk,
wasmKeeper,
DefaultSimulationClearAdminContractSelector,
),
),
simulation.NewWeightedOperation(
weightMsgMigrateContract,
SimulateMsgMigrateContract(
ak,
bk,
wasmKeeper,
DefaultSimulationMigrateContractSelector,
DefaultSimulationMigrateCodeIDSelector,
),
),
}
}
type (
MsgMigrateContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo)
MsgMigrateCodeIDSelector func(sdk.Context, WasmKeeper, uint64) uint64
)
func DefaultSimulationMigrateContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) (sdk.AccAddress, types.ContractInfo) {
var contractAddress sdk.AccAddress
var contractInfo types.ContractInfo
wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool {
if info.Admin != adminAddress {
return false
}
contractAddress = address
contractInfo = info
return true
})
return contractAddress, contractInfo
}
func DefaultSimulationMigrateCodeIDSelector(ctx sdk.Context, wasmKeeper WasmKeeper, currentCodeID uint64) uint64 {
var codeID uint64
wasmKeeper.IterateCodeInfos(ctx, func(u uint64, info types.CodeInfo) bool {
if (info.InstantiateConfig.Permission != types.AccessTypeEverybody) || (u == currentCodeID) {
return false
}
codeID = u
return true
})
return codeID
}
func SimulateMsgMigrateContract(
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
contractSelector MsgMigrateContractSelector,
codeIDSelector MsgMigrateCodeIDSelector,
) simtypes.Operation {
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accs []simtypes.Account,
chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
ctAddress, info := contractSelector(ctx, wasmKeeper, simAccount.Address.String())
if ctAddress == nil {
return simtypes.NoOpMsg(types.ModuleName, types.MsgMigrateContract{}.Type(), "no contract instance available"), nil, nil
}
codeID := codeIDSelector(ctx, wasmKeeper, info.CodeID)
if codeID == 0 {
return simtypes.NoOpMsg(types.ModuleName, types.MsgMigrateContract{}.Type(), "no target contract available"), nil, nil
}
migrateMsg := types.MsgMigrateContract{
Sender: simAccount.Address.String(),
Contract: ctAddress.String(),
CodeID: codeID,
Msg: []byte(`{}`),
}
txCtx := BuildOperationInput(r, app, ctx, &migrateMsg, simAccount, ak, bk, nil)
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
type MsgClearAdminContractSelector func(sdk.Context, WasmKeeper, string) sdk.AccAddress
func DefaultSimulationClearAdminContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) sdk.AccAddress {
var ctAddress sdk.AccAddress
wasmKeeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, info types.ContractInfo) bool {
if info.Admin != adminAddress {
return false
}
ctAddress = addr
return true
})
return ctAddress
}
func SimulateMsgClearAdmin(
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
contractSelector MsgClearAdminContractSelector,
) simtypes.Operation {
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accounts []simtypes.Account,
chainID string,
) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) {
simAccount, _ := simtypes.RandomAcc(r, accounts)
ctAddress := contractSelector(ctx, wasmKeeper, simAccount.Address.String())
if ctAddress == nil {
return simtypes.NoOpMsg(types.ModuleName, types.MsgClearAdmin{}.Type(), "no contract instance available"), nil, nil
}
msg := types.MsgClearAdmin{
Sender: simAccount.Address.String(),
Contract: ctAddress.String(),
}
txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil)
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
type MsgUpdateAdminContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo)
// DefaultSimulationUpdateAdminContractSelector picks the first contract which Admin != ""
func DefaultSimulationUpdateAdminContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) (sdk.AccAddress, types.ContractInfo) {
var contractAddress sdk.AccAddress
var contractInfo types.ContractInfo
wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool {
if info.Admin != adminAddress {
return false
}
contractAddress = address
contractInfo = info
return true
})
return contractAddress, contractInfo
}
func SimulateMsgUpdateAmin(
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
contractSelector MsgUpdateAdminContractSelector,
) simtypes.Operation {
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accs []simtypes.Account,
chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
ctAddress, _ := contractSelector(ctx, wasmKeeper, simAccount.Address.String())
if ctAddress == nil {
return simtypes.NoOpMsg(types.ModuleName, types.MsgUpdateAdmin{}.Type(), "no contract instance available"), nil, nil
}
newAdmin, _ := simtypes.RandomAcc(r, accs)
if newAdmin.Address.String() == simAccount.Address.String() {
return simtypes.NoOpMsg(types.ModuleName, types.MsgUpdateAdmin{}.Type(), "new admin cannot be the same as current admin"), nil, nil
}
msg := types.MsgUpdateAdmin{
Sender: simAccount.Address.String(),
NewAdmin: newAdmin.Address.String(),
Contract: ctAddress.String(),
}
txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil)
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// SimulateMsgStoreCode generates a MsgStoreCode with random values
func SimulateMsgStoreCode(
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
wasmBz []byte,
) simtypes.Operation {
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accs []simtypes.Account,
chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
if wasmKeeper.GetParams(ctx).CodeUploadAccess.Permission != types.AccessTypeEverybody {
return simtypes.NoOpMsg(types.ModuleName, types.MsgStoreCode{}.Type(), "no chain permission"), nil, nil
}
simAccount, _ := simtypes.RandomAcc(r, accs)
permission := wasmKeeper.GetParams(ctx).InstantiateDefaultPermission
config := permission.With(simAccount.Address)
msg := types.MsgStoreCode{
Sender: simAccount.Address.String(),
WASMByteCode: wasmBz,
InstantiatePermission: &config,
}
txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil)
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// CodeIDSelector returns code id to be used in simulations
type CodeIDSelector = func(ctx sdk.Context, wasmKeeper WasmKeeper) uint64
// DefaultSimulationCodeIDSelector picks the first code id
func DefaultSimulationCodeIDSelector(ctx sdk.Context, wasmKeeper WasmKeeper) uint64 {
var codeID uint64
wasmKeeper.IterateCodeInfos(ctx, func(u uint64, info types.CodeInfo) bool {
if info.InstantiateConfig.Permission != types.AccessTypeEverybody {
return false
}
codeID = u
return true
})
return codeID
}
// SimulateMsgInstantiateContract generates a MsgInstantiateContract with random values
func SimulateMsgInstantiateContract(
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
codeSelector CodeIDSelector,
) simtypes.Operation {
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accs []simtypes.Account,
chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount, _ := simtypes.RandomAcc(r, accs)
codeID := codeSelector(ctx, wasmKeeper)
if codeID == 0 {
return simtypes.NoOpMsg(types.ModuleName, types.MsgInstantiateContract{}.Type(), "no codes with permission available"), nil, nil
}
deposit := sdk.Coins{}
spendableCoins := bk.SpendableCoins(ctx, simAccount.Address)
for _, v := range spendableCoins {
if bk.IsSendEnabledCoin(ctx, v) {
deposit = deposit.Add(simtypes.RandSubsetCoins(r, sdk.NewCoins(v))...)
}
}
adminAccount, _ := simtypes.RandomAcc(r, accs)
msg := types.MsgInstantiateContract{
Sender: simAccount.Address.String(),
Admin: adminAccount.Address.String(),
CodeID: codeID,
Label: simtypes.RandStringOfLength(r, 10),
Msg: []byte(`{}`),
Funds: deposit,
}
txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, deposit)
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// MsgExecuteContractSelector returns contract address to be used in simulations
type MsgExecuteContractSelector = func(ctx sdk.Context, wasmKeeper WasmKeeper) sdk.AccAddress
// MsgExecutePayloader extension point to modify msg with custom payload
type MsgExecutePayloader func(msg *types.MsgExecuteContract) error
// MsgExecuteSenderSelector extension point that returns the sender address
type MsgExecuteSenderSelector func(wasmKeeper WasmKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, accs []simtypes.Account) (simtypes.Account, error)
// SimulateMsgExecuteContract create a execute message a reflect contract instance
func SimulateMsgExecuteContract(
ak types.AccountKeeper,
bk BankKeeper,
wasmKeeper WasmKeeper,
contractSelector MsgExecuteContractSelector,
senderSelector MsgExecuteSenderSelector,
payloader MsgExecutePayloader,
) simtypes.Operation {
return func(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
accs []simtypes.Account,
chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
contractAddr := contractSelector(ctx, wasmKeeper)
if contractAddr == nil {
return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "no contract instance available"), nil, nil
}
simAccount, err := senderSelector(wasmKeeper, ctx, contractAddr, accs)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "query contract owner"), nil, err
}
deposit := sdk.Coins{}
spendableCoins := bk.SpendableCoins(ctx, simAccount.Address)
for _, v := range spendableCoins {
if bk.IsSendEnabledCoin(ctx, v) {
deposit = deposit.Add(simtypes.RandSubsetCoins(r, sdk.NewCoins(v))...)
}
}
if deposit.IsZero() {
return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "broke account"), nil, nil
}
msg := types.MsgExecuteContract{
Sender: simAccount.Address.String(),
Contract: contractAddr.String(),
Funds: deposit,
}
if err := payloader(&msg); err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "contract execute payload"), nil, err
}
txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, deposit)
return simulation.GenAndDeliverTxWithRandFees(txCtx)
}
}
// BuildOperationInput helper to build object
func BuildOperationInput(
r *rand.Rand,
app *baseapp.BaseApp,
ctx sdk.Context,
msg interface {
sdk.Msg
Type() string
},
simAccount simtypes.Account,
ak types.AccountKeeper,
bk BankKeeper,
deposit sdk.Coins,
) simulation.OperationInput {
interfaceRegistry := codectypes.NewInterfaceRegistry()
txConfig := tx.NewTxConfig(codec.NewProtoCodec(interfaceRegistry), tx.DefaultSignModes)
return simulation.OperationInput{
R: r,
App: app,
TxGen: txConfig,
Cdc: nil,
Msg: msg,
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: deposit,
}
}
// DefaultSimulationExecuteContractSelector picks the first contract address
func DefaultSimulationExecuteContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper) sdk.AccAddress {
var r sdk.AccAddress
wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool {
r = address
return true
})
return r
}
// DefaultSimulationExecuteSenderSelector queries reflect contract for owner address and selects accounts
func DefaultSimulationExecuteSenderSelector(wasmKeeper WasmKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, accs []simtypes.Account) (simtypes.Account, error) {
var none simtypes.Account
bz, err := json.Marshal(testdata.ReflectQueryMsg{Owner: &struct{}{}})
if err != nil {
return none, errorsmod.Wrap(err, "build smart query")
}
got, err := wasmKeeper.QuerySmart(ctx, contractAddr, bz)
if err != nil {
return none, errorsmod.Wrap(err, "exec smart query")
}
var ownerRes testdata.OwnerResponse
if err := json.Unmarshal(got, &ownerRes); err != nil || ownerRes.Owner == "" {
return none, errorsmod.Wrap(err, "parse smart query response")
}
ownerAddr, err := sdk.AccAddressFromBech32(ownerRes.Owner)
if err != nil {
return none, errorsmod.Wrap(err, "parse contract owner address")
}
simAccount, ok := simtypes.FindAccount(accs, ownerAddr)
if !ok {
return none, errorsmod.Wrap(err, "unknown contract owner address")
}
return simAccount, nil
}
// DefaultSimulationExecutePayloader implements a bank msg to send the
// tokens from contract account back to original sender
func DefaultSimulationExecutePayloader(msg *types.MsgExecuteContract) error {
reflectSend := testdata.ReflectHandleMsg{
Reflect: &testdata.ReflectPayload{
Msgs: []wasmvmtypes.CosmosMsg{{
Bank: &wasmvmtypes.BankMsg{
Send: &wasmvmtypes.SendMsg{
ToAddress: msg.Sender, //
Amount: wasmkeeper.ConvertSdkCoinsToWasmCoins(msg.Funds),
},
},
}},
},
}
reflectSendBz, err := json.Marshal(reflectSend)
if err != nil {
return err
}
msg.Msg = reflectSendBz
return nil
}