From 3da4657102abc0ff7b610bcdbef752f9afe9b852 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Sat, 11 Apr 2020 20:54:45 -0700 Subject: [PATCH] [R4R] BEP3 simulations (#423) * implement randomized genesis, params * implement operations: MsgCreateAtomicSwap * implement claim, refund future ops * remove dynamic block locks * refactor BondedAddresses * add consistent supported assets --- app/sim_test.go | 13 +++ x/bep3/simulation/genesis.go | 160 ++++++++++++++++++++++++++-- x/bep3/simulation/operations/msg.go | 153 ++++++++++++++++++++++++++ x/bep3/simulation/params.go | 38 ++++++- 4 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 x/bep3/simulation/operations/msg.go diff --git a/app/sim_test.go b/app/sim_test.go index ede0aaf1..db6a797b 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -34,6 +34,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking" stakingsimops "github.com/cosmos/cosmos-sdk/x/staking/simulation/operations" "github.com/cosmos/cosmos-sdk/x/supply" + bep3simops "github.com/kava-labs/kava/x/bep3/simulation/operations" ) // Simulation parameter constants @@ -56,6 +57,7 @@ const ( OpWeightMsgUndelegate = "op_weight_msg_undelegate" OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate" OpWeightMsgUnjail = "op_weight_msg_unjail" + OpWeightMsgCreateAtomicSwap = "op_weight_msg_create_atomic_Swap" ) // TestMain runs setup and teardown code before all tests. @@ -264,6 +266,17 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper }(nil), slashingsimops.SimulateMsgUnjail(app.slashingKeeper), }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(app.cdc, OpWeightMsgCreateAtomicSwap, &v, nil, + func(_ *rand.Rand) { + v = 100 + }) + return v + }(nil), + bep3simops.SimulateMsgCreateAtomicSwap(app.accountKeeper, app.bep3Keeper), + }, } } diff --git a/x/bep3/simulation/genesis.go b/x/bep3/simulation/genesis.go index 5daaab0e..b5402736 100644 --- a/x/bep3/simulation/genesis.go +++ b/x/bep3/simulation/genesis.go @@ -2,21 +2,167 @@ package simulation import ( "fmt" + "math/rand" + "strings" + "time" "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/supply" "github.com/kava-labs/kava/x/bep3/types" ) +// Simulation parameter constants +const ( + BnbDeputyAddress = "bnb_deputy_address" + MinBlockLock = "min_block_lock" + MaxBlockLock = "max_block_lock" + SupportedAssets = "supported_assets" +) + +var ( + MaxSupplyLimit = sdk.NewInt(10000000000000000) + Accs []simulation.Account + ConsistentDenoms = [3]string{"bnb", "xrp", "btc"} +) + +// GenBnbDeputyAddress randomized BnbDeputyAddress +func GenBnbDeputyAddress(r *rand.Rand) sdk.AccAddress { + return simulation.RandomAcc(r, Accs).Address +} + +// GenMinBlockLock randomized MinBlockLock +func GenMinBlockLock(r *rand.Rand) int64 { + min := int(types.AbsoluteMinimumBlockLock) + max := int(types.AbsoluteMaximumBlockLock) + return int64(r.Intn(max-min) + min) +} + +// GenMaxBlockLock randomized MaxBlockLock +func GenMaxBlockLock(r *rand.Rand, minBlockLock int64) int64 { + min := int(minBlockLock) + max := int(types.AbsoluteMaximumBlockLock) + return int64(r.Intn(max-min) + min) +} + +// GenSupportedAssets gets randomized SupportedAssets +func GenSupportedAssets(r *rand.Rand) types.AssetParams { + var assets types.AssetParams + for i := 0; i < (r.Intn(10) + 1); i++ { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + denom := strings.ToLower(simulation.RandStringOfLength(r, (r.Intn(3) + 3))) + asset := genSupportedAsset(r, denom) + assets = append(assets, asset) + } + // Add bnb, btc, or xrp as a supported asset for interactions with other modules + stableAsset := genSupportedAsset(r, ConsistentDenoms[r.Intn(3)]) + assets = append(assets, stableAsset) + + return assets +} + +func genSupportedAsset(r *rand.Rand, denom string) types.AssetParam { + coinID, _ := simulation.RandPositiveInt(r, sdk.NewInt(100000)) + limit, _ := simulation.RandPositiveInt(r, MaxSupplyLimit) + return types.AssetParam{ + Denom: denom, + CoinID: int(coinID.Int64()), + Limit: limit, + Active: true, + } +} + // RandomizedGenState generates a random GenesisState func RandomizedGenState(simState *module.SimulationState) { + Accs = simState.Accounts - // TODO implement this fully - // - randomly generating the genesis params - // - overwriting with genesis provided to simulation - genesisState := types.DefaultGenesisState() + bep3Genesis := loadRandomBep3GenState(simState) + fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, bep3Genesis)) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(bep3Genesis) - fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState)) - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState) + authGenesis, totalCoins := loadAuthGenState(simState, bep3Genesis) + simState.GenState[auth.ModuleName] = simState.Cdc.MustMarshalJSON(authGenesis) + + // Update supply to match amount of coins in auth + var supplyGenesis supply.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[supply.ModuleName], &supplyGenesis) + for _, deputyCoin := range totalCoins { + supplyGenesis.Supply = supplyGenesis.Supply.Add(deputyCoin) + } + simState.GenState[supply.ModuleName] = simState.Cdc.MustMarshalJSON(supplyGenesis) +} + +func loadRandomBep3GenState(simState *module.SimulationState) types.GenesisState { + bnbDeputyAddress := simulation.RandomAcc(simState.Rand, simState.Accounts).Address + + // min/max block lock are hardcoded to 50/100 for expected -NumBlocks=100 + minBlockLock := int64(types.AbsoluteMinimumBlockLock) + maxBlockLock := minBlockLock * 2 + + var supportedAssets types.AssetParams + simState.AppParams.GetOrGenerate( + simState.Cdc, SupportedAssets, &supportedAssets, simState.Rand, + func(r *rand.Rand) { supportedAssets = GenSupportedAssets(r) }, + ) + + bep3Genesis := types.GenesisState{ + Params: types.Params{ + BnbDeputyAddress: bnbDeputyAddress, + MinBlockLock: minBlockLock, + MaxBlockLock: maxBlockLock, + SupportedAssets: supportedAssets, + }, + } + + return bep3Genesis +} + +func loadAuthGenState(simState *module.SimulationState, bep3Genesis types.GenesisState) ( + auth.GenesisState, []sdk.Coins) { + var authGenesis auth.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[auth.ModuleName], &authGenesis) + + deputy, found := getAccount(authGenesis.Accounts, bep3Genesis.Params.BnbDeputyAddress) + if !found { + panic("deputy address not found in available accounts") + } + + // Load total limit of each supported asset to deputy's account + var totalCoins []sdk.Coins + for _, asset := range bep3Genesis.Params.SupportedAssets { + assetCoin := sdk.NewCoins(sdk.NewCoin(asset.Denom, asset.Limit)) + if err := deputy.SetCoins(deputy.GetCoins().Add(assetCoin)); err != nil { + panic(err) + } + totalCoins = append(totalCoins, assetCoin) + } + authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, deputy) + + return authGenesis, totalCoins +} + +// Return an account from a list of accounts that matches an address. +func getAccount(accounts []authexported.GenesisAccount, addr sdk.AccAddress) (authexported.GenesisAccount, bool) { + for _, a := range accounts { + if a.GetAddress().Equals(addr) { + return a, true + } + } + return nil, false +} + +// In a list of accounts, replace the first account found with the same address. If not found, append the account. +func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount { + newAccounts := accounts + for i, a := range accounts { + if a.GetAddress().Equals(acc.GetAddress()) { + newAccounts[i] = acc + return newAccounts + } + } + return append(newAccounts, acc) } diff --git a/x/bep3/simulation/operations/msg.go b/x/bep3/simulation/operations/msg.go new file mode 100644 index 00000000..2f0fa6a0 --- /dev/null +++ b/x/bep3/simulation/operations/msg.go @@ -0,0 +1,153 @@ +package operations + +import ( + "fmt" + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/x/bep3" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" +) + +var ( + noOpMsg = simulation.NoOpMsg(bep3.ModuleName) +) + +// SimulateMsgCreateAtomicSwap generates a MsgCreateAtomicSwap with random values +func SimulateMsgCreateAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper) simulation.Operation { + handler := bep3.NewHandler(k) + + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) ( + simulation.OperationMsg, []simulation.FutureOperation, error) { + + sender := k.GetBnbDeputyAddress(ctx) + recipient := simulation.RandomAcc(r, accs).Address + + recipientOtherChain := simulation.RandStringOfLength(r, 43) + senderOtherChain := simulation.RandStringOfLength(r, 43) + + // Generate cryptographically strong pseudo-random number + randomNumber, err := types.GenerateSecureRandomNumber() + if err != nil { + return noOpMsg, nil, err + } + // Must use current blocktime instead of 'now' since initial blocktime was randomly generated + timestamp := ctx.BlockTime().Unix() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + // Randomly select an asset from supported assets + assets, found := k.GetAssets(ctx) + if !found { + return noOpMsg, nil, fmt.Errorf("no supported assets found") + } + asset := assets[r.Intn(len(assets))] + + // Check that the sender has coins of this type + availableAmount := ak.GetAccount(ctx, sender).GetCoins().AmountOf(asset.Denom) + if !availableAmount.IsPositive() { + return noOpMsg, nil, fmt.Errorf("available amount must be positive") + } + + // Get a random amount of the available coins + amount, err := simulation.RandPositiveInt(r, availableAmount) + if err != nil { + return noOpMsg, nil, err + } + + // If we don't adjust the conversion factor, we'll be out of funds soon + adjustedAmount := amount.Int64() / int64(math.Pow10(8)) + coin := sdk.NewInt64Coin(asset.Denom, adjustedAmount) + coins := sdk.NewCoins(coin) + expectedIncome := coin.String() + + // We're assuming that sims are run with -NumBlocks=100 + heightSpan := int64(55) + crossChain := true + + msg := types.NewMsgCreateAtomicSwap( + sender, recipient, recipientOtherChain, senderOtherChain, randomNumberHash, + timestamp, coins, expectedIncome, heightSpan, crossChain) + + if err := msg.ValidateBasic(); err != nil { + return noOpMsg, nil, fmt.Errorf("expected MsgCreateAtomicSwap to pass ValidateBasic: %s", err) + } + + // Submit msg + ok := submitMsg(ctx, handler, msg) + + // If created, construct a MsgClaimAtomicSwap or MsgRefundAtomicSwap future operation + var futureOp simulation.FutureOperation + if ok { + swapID := types.CalculateSwapID(msg.RandomNumberHash, msg.From, msg.SenderOtherChain) + acc := simulation.RandomAcc(r, accs) + evenOdd := r.Intn(2) + 1 + if evenOdd%2 == 0 { + // Claim future operation + executionBlock := ctx.BlockHeight() + (msg.HeightSpan / 2) + futureOp = loadClaimFutureOp(acc.Address, swapID, randomNumber.Bytes(), executionBlock, handler) + } else { + // Refund future operation + executionBlock := ctx.BlockHeight() + msg.HeightSpan + futureOp = loadRefundFutureOp(acc.Address, swapID, executionBlock, handler) + } + } + + return simulation.NewOperationMsg(msg, ok, ""), []simulation.FutureOperation{futureOp}, nil + } +} + +func loadClaimFutureOp(sender sdk.AccAddress, swapID []byte, randomNumber []byte, height int64, handler sdk.Handler) simulation.FutureOperation { + claimOp := func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) ( + simulation.OperationMsg, []simulation.FutureOperation, error) { + + // Build the refund msg and validate basic + claimMsg := types.NewMsgClaimAtomicSwap(sender, swapID, randomNumber) + if err := claimMsg.ValidateBasic(); err != nil { + return noOpMsg, nil, fmt.Errorf("expected MsgClaimAtomicSwap to pass ValidateBasic: %s", err) + } + + // Test msg submission at target block height + ok := handler(ctx.WithBlockHeight(height), claimMsg).IsOK() + return simulation.NewOperationMsg(claimMsg, ok, ""), nil, nil + } + + return simulation.FutureOperation{ + BlockHeight: int(height), + Op: claimOp, + } +} + +func loadRefundFutureOp(sender sdk.AccAddress, swapID []byte, height int64, handler sdk.Handler) simulation.FutureOperation { + refundOp := func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) ( + simulation.OperationMsg, []simulation.FutureOperation, error) { + // Build the refund msg and validate basic + refundMsg := types.NewMsgRefundAtomicSwap(sender, swapID) + if err := refundMsg.ValidateBasic(); err != nil { + return noOpMsg, nil, fmt.Errorf("expected MsgRefundAtomicSwap to pass ValidateBasic: %s", err) + } + + // Test msg submission at target block height + ok := handler(ctx.WithBlockHeight(height), refundMsg).IsOK() + return simulation.NewOperationMsg(refundMsg, ok, ""), nil, nil + } + + return simulation.FutureOperation{ + BlockHeight: int(height), + Op: refundOp, + } +} + +func submitMsg(ctx sdk.Context, handler sdk.Handler, msg sdk.Msg) (ok bool) { + ctx, write := ctx.CacheContext() + ok = handler(ctx, msg).IsOK() + if ok { + write() + } + return ok +} diff --git a/x/bep3/simulation/params.go b/x/bep3/simulation/params.go index 8c1f7aff..41c3a346 100644 --- a/x/bep3/simulation/params.go +++ b/x/bep3/simulation/params.go @@ -1,14 +1,46 @@ package simulation import ( + "fmt" "math/rand" "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/x/bep3/types" +) + +const ( + keyBnbDeputyAddress = "BnbDeputyAddress" + keyMinBlockLock = "MinBlockLock" + keyMaxBlockLock = "MaxBlockLock" + keySupportedAssets = "SupportedAssets" ) // ParamChanges defines the parameters that can be modified by param change proposals -// on the simulation func ParamChanges(r *rand.Rand) []simulation.ParamChange { - // TODO implement this - return []simulation.ParamChange{} + // We generate MinBlockLock first because the result is required by GenMaxBlockLock() + minBlockLockVal := GenMinBlockLock(r) + + return []simulation.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, keyBnbDeputyAddress, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenBnbDeputyAddress(r)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, keyMinBlockLock, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%d\"", minBlockLockVal) + }, + ), + simulation.NewSimParamChange(types.ModuleName, keyMaxBlockLock, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%d\"", GenMaxBlockLock(r, minBlockLockVal)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, keySupportedAssets, "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%v\"", GenSupportedAssets(r)) + }, + ), + } }