feat: add simulation for nft module (#10522)

<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->

## Description

Add nft module simulation and unit test,refer #9826

<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Zhiqiang Zhang 2021-11-11 23:44:41 +08:00 committed by GitHub
parent b6d416d871
commit ba843a351c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 555 additions and 2 deletions

View File

@ -391,6 +391,7 @@ func NewSimApp(
params.NewAppModule(app.ParamsKeeper),
evidence.NewAppModule(app.EvidenceKeeper),
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
nftmodule.NewAppModule(appCodec, app.NFTKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
)
app.sm.RegisterStoreDecoders()

View File

@ -3,6 +3,7 @@ package nft
import (
"context"
"encoding/json"
"math/rand"
"github.com/gorilla/mux"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
@ -15,15 +16,18 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/client/cli"
"github.com/cosmos/cosmos-sdk/x/nft/keeper"
"github.com/cosmos/cosmos-sdk/x/nft/simulation"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic defines the basic application module used by the nft module.
@ -159,3 +163,37 @@ func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {}
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
// ____________________________________________________________________________
// AppModuleSimulation functions
// GenerateGenesisState creates a randomized GenState of the nft module.
func (AppModule) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// ProposalContents returns all the nft content functions used to
// simulate governance proposals.
func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent {
return nil
}
// RandomizedParams creates randomized nft param changes for the simulator.
func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange {
return nil
}
// RegisterStoreDecoder registers a decoder for nft module's types
func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[keeper.StoreKey] = simulation.NewDecodeStore(am.cdc)
}
// WeightedOperations returns the all the nft module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation {
return simulation.WeightedOperations(
am.registry,
simState.AppParams, simState.Cdc,
am.accountKeeper, am.bankKeeper, am.keeper,
)
}

View File

@ -0,0 +1,45 @@
package simulation
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/kv"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/keeper"
)
// NewDecodeStore returns a decoder function closure that umarshals the KVPair's
// Value to the corresponding nft type.
func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string {
return func(kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], keeper.ClassKey):
var classA, classB nft.Class
cdc.MustUnmarshal(kvA.Value, &classA)
cdc.MustUnmarshal(kvB.Value, &classB)
return fmt.Sprintf("%v\n%v", classA, classB)
case bytes.Equal(kvA.Key[:1], keeper.NFTKey):
var nftA, nftB nft.NFT
cdc.MustUnmarshal(kvA.Value, &nftA)
cdc.MustUnmarshal(kvB.Value, &nftB)
return fmt.Sprintf("%v\n%v", nftA, nftB)
case bytes.Equal(kvA.Key[:1], keeper.NFTOfClassByOwnerKey):
return fmt.Sprintf("%v\n%v", kvA.Value, kvB.Value)
case bytes.Equal(kvA.Key[:1], keeper.OwnerKey):
var ownerA, ownerB sdk.AccAddress
ownerA = sdk.AccAddress(kvA.Value)
ownerB = sdk.AccAddress(kvB.Value)
return fmt.Sprintf("%v\n%v", ownerA, ownerB)
case bytes.Equal(kvA.Key[:1], keeper.ClassTotalSupply):
var supplyA, supplyB uint64
supplyA = sdk.BigEndianToUint64(kvA.Value)
supplyB = sdk.BigEndianToUint64(kvB.Value)
return fmt.Sprintf("%v\n%v", supplyA, supplyB)
default:
panic(fmt.Sprintf("invalid nft key %X", kvA.Key))
}
}
}

View File

@ -0,0 +1,84 @@
package simulation_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/kv"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/keeper"
"github.com/cosmos/cosmos-sdk/x/nft/simulation"
)
var (
ownerPk1 = ed25519.GenPrivKey().PubKey()
ownerAddr1 = sdk.AccAddress(ownerPk1.Address())
)
func TestDecodeStore(t *testing.T) {
cdc := simapp.MakeTestEncodingConfig().Codec
dec := simulation.NewDecodeStore(cdc)
class := nft.Class{
Id: "ClassID",
Name: "ClassName",
Symbol: "ClassSymbol",
Description: "ClassDescription",
Uri: "ClassURI",
}
classBz, err := cdc.Marshal(&class)
require.NoError(t, err)
nft := nft.NFT{
ClassId: "ClassID",
Id: "NFTID",
Uri: "NFTURI",
}
nftBz, err := cdc.Marshal(&nft)
require.NoError(t, err)
nftOfClassByOwnerValue := []byte{0x01}
totalSupply := 1
totalSupplyBz := sdk.Uint64ToBigEndian(1)
kvPairs := kv.Pairs{
Pairs: []kv.Pair{
{Key: []byte(keeper.ClassKey), Value: classBz},
{Key: []byte(keeper.NFTKey), Value: nftBz},
{Key: []byte(keeper.NFTOfClassByOwnerKey), Value: nftOfClassByOwnerValue},
{Key: []byte(keeper.OwnerKey), Value: ownerAddr1},
{Key: []byte(keeper.ClassTotalSupply), Value: totalSupplyBz},
{Key: []byte{0x99}, Value: []byte{0x99}},
},
}
tests := []struct {
name string
expectErr bool
expectedLog string
}{
{"Class", false, fmt.Sprintf("%v\n%v", class, class)},
{"NFT", false, fmt.Sprintf("%v\n%v", nft, nft)},
{"NFTOfClassByOwnerKey", false, fmt.Sprintf("%v\n%v", nftOfClassByOwnerValue, nftOfClassByOwnerValue)},
{"OwnerKey", false, fmt.Sprintf("%v\n%v", ownerAddr1, ownerAddr1)},
{"ClassTotalSupply", false, fmt.Sprintf("%v\n%v", totalSupply, totalSupply)},
{"other", true, ""},
}
for i, tt := range tests {
i, tt := i, tt
t.Run(tt.name, func(t *testing.T) {
if tt.expectErr {
require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name)
} else {
require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name)
}
})
}
}

View File

@ -0,0 +1,67 @@
package simulation
import (
"math/rand"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/nft"
)
// genClasses returns a slice of nft class.
func genClasses(r *rand.Rand, accounts []simtypes.Account) []*nft.Class {
classes := make([]*nft.Class, len(accounts)-1)
for i := 0; i < len(accounts)-1; i++ {
classes[i] = &nft.Class{
Id: simtypes.RandStringOfLength(r, 10),
Name: simtypes.RandStringOfLength(r, 10),
Symbol: simtypes.RandStringOfLength(r, 10),
Description: simtypes.RandStringOfLength(r, 10),
Uri: simtypes.RandStringOfLength(r, 10),
}
}
return classes
}
// genNFT returns a slice of nft.
func genNFT(r *rand.Rand, classID string, accounts []simtypes.Account) []*nft.Entry {
entries := make([]*nft.Entry, len(accounts)-1)
for i := 0; i < len(accounts)-1; i++ {
owner := accounts[i]
entries[i] = &nft.Entry{
Owner: owner.Address.String(),
Nfts: []*nft.NFT{
{
ClassId: classID,
Id: simtypes.RandStringOfLength(r, 10),
Uri: simtypes.RandStringOfLength(r, 10),
},
},
}
}
return entries
}
// RandomizedGenState generates a random GenesisState for nft.
func RandomizedGenState(simState *module.SimulationState) {
var classes []*nft.Class
simState.AppParams.GetOrGenerate(
simState.Cdc, "nft", &classes, simState.Rand,
func(r *rand.Rand) { classes = genClasses(r, simState.Accounts) },
)
var entries []*nft.Entry
simState.AppParams.GetOrGenerate(
simState.Cdc, "nft", &entries, simState.Rand,
func(r *rand.Rand) {
class := classes[r.Int63n(int64(len(classes)))]
entries = genNFT(r, class.Id, simState.Accounts)
},
)
nftGenesis := &nft.GenesisState{
Classes: classes,
Entries: entries,
}
simState.GenState[nft.ModuleName] = simState.Cdc.MustMarshalJSON(nftGenesis)
}

View File

@ -0,0 +1,39 @@
package simulation_test
import (
"encoding/json"
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/simulation"
)
func TestRandomizedGenState(t *testing.T) {
app := simapp.Setup(t, false)
s := rand.NewSource(1)
r := rand.New(s)
simState := module.SimulationState{
AppParams: make(simtypes.AppParams),
Cdc: app.AppCodec(),
Rand: r,
NumBonded: 3,
Accounts: simtypes.RandomAccounts(r, 3),
InitialStake: 1000,
GenState: make(map[string]json.RawMessage),
}
simulation.RandomizedGenState(&simState)
var nftGenesis nft.GenesisState
simState.Cdc.MustUnmarshalJSON(simState.GenState[nft.ModuleName], &nftGenesis)
require.Len(t, nftGenesis.Classes, len(simState.Accounts)-1)
require.Len(t, nftGenesis.Entries, len(simState.Accounts)-1)
}

View File

@ -0,0 +1,164 @@
package simulation
import (
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
simappparams "github.com/cosmos/cosmos-sdk/simapp/params"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/keeper"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
const (
// Simulation operation weights constants
OpWeightMsgSend = "op_weight_msg_send"
)
const (
// nft operations weights
WeightSend = 100
)
var (
TypeMsgSend = sdk.MsgTypeURL(&nft.MsgSend{})
)
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
registry cdctypes.InterfaceRegistry,
appParams simtypes.AppParams,
cdc codec.JSONCodec,
ak nft.AccountKeeper,
bk nft.BankKeeper,
k keeper.Keeper) simulation.WeightedOperations {
var (
weightMsgSend int
)
appParams.GetOrGenerate(cdc, OpWeightMsgSend, &weightMsgSend, nil,
func(_ *rand.Rand) {
weightMsgSend = WeightSend
},
)
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgSend,
SimulateMsgSend(codec.NewProtoCodec(registry), ak, bk, k),
),
}
}
// SimulateMsgSend generates a MsgSend with random values.
func SimulateMsgSend(
cdc *codec.ProtoCodec,
ak nft.AccountKeeper,
bk nft.BankKeeper,
k keeper.Keeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
sender, _ := simtypes.RandomAcc(r, accs)
receiver, _ := simtypes.RandomAcc(r, accs)
if sender.Address.Equals(receiver.Address) {
return simtypes.NoOpMsg(nft.ModuleName, TypeMsgSend, "sender and receiver are same"), nil, nil
}
senderAcc := ak.GetAccount(ctx, sender.Address)
spendableCoins := bk.SpendableCoins(ctx, sender.Address)
fees, err := simtypes.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simtypes.NoOpMsg(nft.ModuleName, TypeMsgSend, err.Error()), nil, err
}
spendLimit := spendableCoins.Sub(fees)
if spendLimit == nil {
return simtypes.NoOpMsg(nft.ModuleName, TypeMsgSend, "spend limit is nil"), nil, nil
}
n, err := randNFT(ctx, r, k, senderAcc.GetAddress())
if err != nil {
return simtypes.NoOpMsg(nft.ModuleName, TypeMsgSend, err.Error()), nil, err
}
msg := &nft.MsgSend{
ClassId: n.ClassId,
Id: n.Id,
Sender: senderAcc.GetAddress().String(),
Receiver: receiver.Address.String(),
}
txCfg := simappparams.MakeTestEncodingConfig().TxConfig
tx, err := helpers.GenTx(
txCfg,
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{senderAcc.GetAccountNumber()},
[]uint64{senderAcc.GetSequence()},
sender.PrivKey,
)
if err != nil {
return simtypes.NoOpMsg(nft.ModuleName, TypeMsgSend, "unable to generate mock tx"), nil, err
}
_, _, err = app.SimDeliver(txCfg.TxEncoder(), tx)
if err != nil {
return simtypes.NoOpMsg(nft.ModuleName, sdk.MsgTypeURL(msg), "unable to deliver tx"), nil, err
}
return simtypes.NewOperationMsg(msg, true, "", cdc), nil, err
}
}
func randNFT(ctx sdk.Context, r *rand.Rand, k keeper.Keeper, minter sdk.AccAddress) (nft.NFT, error) {
c, err := randClass(ctx, r, k)
if err != nil {
return nft.NFT{}, err
}
ns := k.GetNFTsOfClassByOwner(ctx, c.Id, minter)
if len(ns) > 0 {
return ns[r.Intn(len(ns))], nil
}
n := nft.NFT{
ClassId: c.Id,
Id: simtypes.RandStringOfLength(r, 10),
Uri: simtypes.RandStringOfLength(r, 10),
}
err = k.Mint(ctx, n, minter)
if err != nil {
return nft.NFT{}, err
}
return n, nil
}
func randClass(ctx sdk.Context, r *rand.Rand, k keeper.Keeper) (nft.Class, error) {
classes := k.GetClasses(ctx)
if len(classes) == 0 {
c := nft.Class{
Id: simtypes.RandStringOfLength(r, 10),
Name: simtypes.RandStringOfLength(r, 10),
Symbol: simtypes.RandStringOfLength(r, 10),
Description: simtypes.RandStringOfLength(r, 10),
Uri: simtypes.RandStringOfLength(r, 10),
}
err := k.SaveClass(ctx, c)
if err != nil {
return nft.Class{}, err
}
return c, nil
}
return *classes[r.Intn(len(classes))], nil
}

View File

@ -0,0 +1,115 @@
package simulation_test
import (
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/bank/testutil"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/simulation"
)
type SimTestSuite struct {
suite.Suite
ctx sdk.Context
app *simapp.SimApp
}
func (suite *SimTestSuite) SetupTest() {
checkTx := false
app := simapp.Setup(suite.T(), checkTx)
suite.app = app
suite.ctx = app.BaseApp.NewContext(checkTx, tmproto.Header{})
}
func (suite *SimTestSuite) TestWeightedOperations() {
weightedOps := simulation.WeightedOperations(
suite.app.InterfaceRegistry(),
make(simtypes.AppParams),
suite.app.AppCodec(),
suite.app.AccountKeeper,
suite.app.BankKeeper, suite.app.NFTKeeper,
)
// setup 3 accounts
s := rand.NewSource(1)
r := rand.New(s)
accs := suite.getTestingAccounts(r, 3)
expected := []struct {
weight int
opMsgRoute string
opMsgName string
}{
{simulation.WeightSend, simulation.TypeMsgSend, simulation.TypeMsgSend},
}
for i, w := range weightedOps {
operationMsg, _, _ := w.Op()(r, suite.app.BaseApp, suite.ctx, accs, "")
// the following checks are very much dependent from the ordering of the output given
// by WeightedOperations. if the ordering in WeightedOperations changes some tests
// will fail
suite.Require().Equal(expected[i].weight, w.Weight(), "weight should be the same")
suite.Require().Equal(expected[i].opMsgRoute, operationMsg.Route, "route should be the same")
suite.Require().Equal(expected[i].opMsgName, operationMsg.Name, "operation Msg name should be the same")
}
}
func (suite *SimTestSuite) getTestingAccounts(r *rand.Rand, n int) []simtypes.Account {
accounts := simtypes.RandomAccounts(r, n)
initAmt := suite.app.StakingKeeper.TokensFromConsensusPower(suite.ctx, 200000)
initCoins := sdk.NewCoins(sdk.NewCoin("stake", initAmt))
// add coins to the accounts
for _, account := range accounts {
acc := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, account.Address)
suite.app.AccountKeeper.SetAccount(suite.ctx, acc)
suite.Require().NoError(testutil.FundAccount(suite.app.BankKeeper, suite.ctx, account.Address, initCoins))
}
return accounts
}
func (suite *SimTestSuite) TestSimulateMsgSend() {
s := rand.NewSource(1)
r := rand.New(s)
accounts := suite.getTestingAccounts(r, 2)
blockTime := time.Now().UTC()
ctx := suite.ctx.WithBlockTime(blockTime)
// begin a new block
suite.app.BeginBlock(abci.RequestBeginBlock{
Header: tmproto.Header{
Height: suite.app.LastBlockHeight() + 1,
AppHash: suite.app.LastCommitID().Hash,
},
})
// execute operation
registry := suite.app.InterfaceRegistry()
op := simulation.SimulateMsgSend(codec.NewProtoCodec(registry), suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.NFTKeeper)
operationMsg, futureOperations, err := op(r, suite.app.BaseApp, ctx, accounts, "")
suite.Require().NoError(err)
var msg nft.MsgSend
suite.app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg)
suite.Require().True(operationMsg.OK)
suite.Require().Len(futureOperations, 0)
}
func TestSimTestSuite(t *testing.T) {
suite.Run(t, new(SimTestSuite))
}