From f4756f8072281f03331122a6dbc1d8988a368565 Mon Sep 17 00:00:00 2001 From: Marko Date: Tue, 5 Mar 2024 10:43:23 +0100 Subject: [PATCH 1/2] docs: add section on creating a testnets from mainnet exports (#19475) (cherry picked from commit 89bba66111f0d15fb6c142df82a4a4089f20784d) # Conflicts: # docs/docs/build/building-apps/05-app-testnet.md --- .../build/building-apps/05-app-testnet.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/docs/build/building-apps/05-app-testnet.md diff --git a/docs/docs/build/building-apps/05-app-testnet.md b/docs/docs/build/building-apps/05-app-testnet.md new file mode 100644 index 000000000000..c799236872d7 --- /dev/null +++ b/docs/docs/build/building-apps/05-app-testnet.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 1 +--- + +# Application Testnets + +Building an application is complicated and requires a lot of testing. The Cosmos SDK provides a way to test your application in a real-world environment: a testnet. + +We allow developers to take the state from their mainnet and run tests against the state. This is useful for testing upgrade migrations, or for testing the application in a real-world environment. + +## Testnet Setup + +We will be breaking down the steps to create a testnet from mainnet state. + +```go + // InitMerlinAppForTestnet is broken down into two sections: + // Required Changes: Changes that, if not made, will cause the testnet to halt or panic + // Optional Changes: Changes to customize the testnet to one's liking (lower vote times, fund accounts, etc) + func InitMerlinAppForTestnet(app *MerlinApp, newValAddr bytes.HexBytes, newValPubKey crypto.PubKey, newOperatorAddress, upgradeToTrigger string) *MerlinApp { + ... + } +``` + +### Required Changes + +#### Staking + +When creating a testnet the important part is migrate the validator set from many validators to one or a few. This allows developers to spin up the chain without needing to replace validator keys. + +```go + ctx := app.BaseApp.NewUncachedContext(true, tmproto.Header{}) + pubkey := &ed25519.PubKey{Key: newValPubKey.Bytes()} + pubkeyAny, err := types.NewAnyWithValue(pubkey) + if err != nil { + tmos.Exit(err.Error()) + } + + // STAKING + // + + // Create Validator struct for our new validator. + _, bz, err := bech32.DecodeAndConvert(newOperatorAddress) + if err != nil { + tmos.Exit(err.Error()) + } + bech32Addr, err := bech32.ConvertAndEncode("simvaloper", bz) + if err != nil { + tmos.Exit(err.Error()) + } + newVal := stakingtypes.Validator{ + OperatorAddress: bech32Addr, + ConsensusPubkey: pubkeyAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: sdk.NewInt(900000000000000), + DelegatorShares: sdk.MustNewDecFromStr("10000000"), + Description: stakingtypes.Description{ + Moniker: "Testnet Validator", + }, + Commission: stakingtypes.Commission{ + CommissionRates: stakingtypes.CommissionRates{ + Rate: sdk.MustNewDecFromStr("0.05"), + MaxRate: sdk.MustNewDecFromStr("0.1"), + MaxChangeRate: sdk.MustNewDecFromStr("0.05"), + }, + }, + MinSelfDelegation: sdk.OneInt(), + } + + // Remove all validators from power store + stakingKey := app.GetKey(stakingtypes.ModuleName) + stakingStore := ctx.KVStore(stakingKey) + iterator := app.StakingKeeper.ValidatorsPowerStoreIterator(ctx) + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all valdiators from last validators store + iterator = app.StakingKeeper.LastValidatorsIterator(ctx) + for ; iterator.Valid(); iterator.Next() { + app.StakingKeeper.LastValidatorPower.Delete(iterator.Key()) + } + iterator.Close() + + // Add our validator to power and last validators store + app.StakingKeeper.SetValidator(ctx, newVal) + err = app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal) + if err != nil { + panic(err) + } + app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal) + app.StakingKeeper.SetLastValidatorPower(ctx, newVal.GetOperator(), 0) + if err := app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, newVal.GetOperator()); err != nil { + panic(err) + } +``` + +#### Distribution + +Since the validator set has changed, we need to update the distribution records for the new validator. + + +```go + // Initialize records for this validator across all distribution stores + app.DistrKeeper.ValidatorHistoricalRewards.Set(ctx, newVal.GetOperator(), 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1)) + app.DistrKeeper.ValidatorCurrentRewards.Set(ctx, newVal.GetOperator(), distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)) + app.DistrKeeper.ValidatorAccumulatedCommission.Set(ctx, newVal.GetOperator(), distrtypes.InitialValidatorAccumulatedCommission()) + app.DistrKeeper.ValidatorOutstandingRewards.Set(ctx, newVal.GetOperator(), distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) +``` + +#### Slashing + +We also need to set the validator signing info for the new validator. + +```go + // SLASHING + // + + // Set validator signing info for our new validator. + newConsAddr := sdk.ConsAddress(newValAddr.Bytes()) + newValidatorSigningInfo := slashingtypes.ValidatorSigningInfo{ + Address: newConsAddr.String(), + StartHeight: app.LastBlockHeight() - 1, + Tombstoned: false, + } + app.SlashingKeeper.ValidatorSigningInfo.Set(ctx, newConsAddr, newValidatorSigningInfo) +``` + +#### Bank + +It is useful to create new accounts for your testing purposes. This avoids the need to have the same key as you may have on mainnet. + +```go + // BANK + // + + defaultCoins := sdk.NewCoins(sdk.NewInt64Coin("ustake", 1000000000000)) + + localMerlinAccounts := []sdk.AccAddress{ + sdk.MustAccAddressFromBech32("cosmos12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj"), + sdk.MustAccAddressFromBech32("cosmos1cyyzpxplxdzkeea7kwsydadg87357qnahakaks"), + sdk.MustAccAddressFromBech32("cosmos18s5lynnmx37hq4wlrw9gdn68sg2uxp5rgk26vv"), + sdk.MustAccAddressFromBech32("cosmos1qwexv7c6sm95lwhzn9027vyu2ccneaqad4w8ka"), + sdk.MustAccAddressFromBech32("cosmos14hcxlnwlqtq75ttaxf674vk6mafspg8xwgnn53"), + sdk.MustAccAddressFromBech32("cosmos12rr534cer5c0vj53eq4y32lcwguyy7nndt0u2t"), + sdk.MustAccAddressFromBech32("cosmos1nt33cjd5auzh36syym6azgc8tve0jlvklnq7jq"), + sdk.MustAccAddressFromBech32("cosmos10qfrpash5g2vk3hppvu45x0g860czur8ff5yx0"), + sdk.MustAccAddressFromBech32("cosmos1f4tvsdukfwh6s9swrc24gkuz23tp8pd3e9r5fa"), + sdk.MustAccAddressFromBech32("cosmos1myv43sqgnj5sm4zl98ftl45af9cfzk7nhjxjqh"), + sdk.MustAccAddressFromBech32("cosmos14gs9zqh8m49yy9kscjqu9h72exyf295afg6kgk"), + sdk.MustAccAddressFromBech32("cosmos1jllfytsz4dryxhz5tl7u73v29exsf80vz52ucc")} + + // Fund localMerlin accounts + for _, account := range localMerlinAccounts { + err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins) + if err != nil { + tmos.Exit(err.Error()) + } + err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins) + if err != nil { + tmos.Exit(err.Error()) + } + } +``` + +#### Upgrade + +If you would like to schedule an upgrade the below can be used. + +```go + // UPGRADE + // + + if upgradeToTrigger != "" { + upgradePlan := upgradetypes.Plan{ + Name: upgradeToTrigger, + Height: app.LastBlockHeight(), + } + err = app.UpgradeKeeper.ScheduleUpgrade(ctx, upgradePlan) + if err != nil { + panic(err) + } + } +``` + +### Optional Changes + +If you have custom modules that rely on specific state from the above modules and/or you would like to test your custom module, you will need to update the state of your custom module to reflect your needs + +## Running the Testnet + +Before we can run the testnet we must plug everything together. + +in `root.go`, in the `initRootCmd` function we add: + +```diff + server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, createMerlinAppAndExport, addModuleInitFlags) + ++ server.AddTestnetCreatorCommand(rootCmd, simapp.DefaultNodeHome, newTestnetApp, addModuleInitFlags) +``` + +Next we will add a newTestnetApp helper function: + +```diff +// newTestnetApp starts by running the normal newApp method. From there, the app interface returned is modified in order +// for a testnet to be created from the provided app. +func newTestnetApp(logger log.Logger, db cometbftdb.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application { + // Create an app and type cast to an MerlinApp + app := newApp(logger, db, traceStore, appOpts) + simApp, ok := app.(*simapp.SimApp) + if !ok { + panic("app created from newApp is not of type simApp") + } + + newValAddr, ok := appOpts.Get(server.KeyNewValAddr).(bytes.HexBytes) + if !ok { + panic("newValAddr is not of type bytes.HexBytes") + } + newValPubKey, ok := appOpts.Get(server.KeyUserPubKey).(crypto.PubKey) + if !ok { + panic("newValPubKey is not of type crypto.PubKey") + } + newOperatorAddress, ok := appOpts.Get(server.KeyNewOpAddr).(string) + if !ok { + panic("newOperatorAddress is not of type string") + } + upgradeToTrigger, ok := appOpts.Get(server.KeyTriggerTestnetUpgrade).(string) + if !ok { + panic("upgradeToTrigger is not of type string") + } + + // Make modifications to the normal MerlinApp required to run the network locally + return meriln.InitMerlinAppForTestnet(simApp, newValAddr, newValPubKey, newOperatorAddress, upgradeToTrigger) +} +``` From 78f31246e7352f8048a45b187fc6c8cd4d8fe43d Mon Sep 17 00:00:00 2001 From: marbar3778 Date: Tue, 5 Mar 2024 13:13:22 +0100 Subject: [PATCH 2/2] rename merlin to simapp --- docs/docs/build/building-apps/05-app-testnet.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/build/building-apps/05-app-testnet.md b/docs/docs/build/building-apps/05-app-testnet.md index c799236872d7..607599cc1952 100644 --- a/docs/docs/build/building-apps/05-app-testnet.md +++ b/docs/docs/build/building-apps/05-app-testnet.md @@ -13,10 +13,10 @@ We allow developers to take the state from their mainnet and run tests against t We will be breaking down the steps to create a testnet from mainnet state. ```go - // InitMerlinAppForTestnet is broken down into two sections: + // InitSimAppForTestnet is broken down into two sections: // Required Changes: Changes that, if not made, will cause the testnet to halt or panic // Optional Changes: Changes to customize the testnet to one's liking (lower vote times, fund accounts, etc) - func InitMerlinAppForTestnet(app *MerlinApp, newValAddr bytes.HexBytes, newValPubKey crypto.PubKey, newOperatorAddress, upgradeToTrigger string) *MerlinApp { + func InitSimAppForTestnet(app *simApp, newValAddr bytes.HexBytes, newValPubKey crypto.PubKey, newOperatorAddress, upgradeToTrigger string) *simApp { ... } ``` @@ -137,7 +137,7 @@ It is useful to create new accounts for your testing purposes. This avoids the n defaultCoins := sdk.NewCoins(sdk.NewInt64Coin("ustake", 1000000000000)) - localMerlinAccounts := []sdk.AccAddress{ + localSimAppAccounts := []sdk.AccAddress{ sdk.MustAccAddressFromBech32("cosmos12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj"), sdk.MustAccAddressFromBech32("cosmos1cyyzpxplxdzkeea7kwsydadg87357qnahakaks"), sdk.MustAccAddressFromBech32("cosmos18s5lynnmx37hq4wlrw9gdn68sg2uxp5rgk26vv"), @@ -151,8 +151,8 @@ It is useful to create new accounts for your testing purposes. This avoids the n sdk.MustAccAddressFromBech32("cosmos14gs9zqh8m49yy9kscjqu9h72exyf295afg6kgk"), sdk.MustAccAddressFromBech32("cosmos1jllfytsz4dryxhz5tl7u73v29exsf80vz52ucc")} - // Fund localMerlin accounts - for _, account := range localMerlinAccounts { + // Fund localSimApp accounts + for _, account := range localsimAccounts { err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins) if err != nil { tmos.Exit(err.Error()) @@ -195,7 +195,7 @@ Before we can run the testnet we must plug everything together. in `root.go`, in the `initRootCmd` function we add: ```diff - server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, createMerlinAppAndExport, addModuleInitFlags) + server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, createsimAppAndExport, addModuleInitFlags) ++ server.AddTestnetCreatorCommand(rootCmd, simapp.DefaultNodeHome, newTestnetApp, addModuleInitFlags) ``` @@ -205,7 +205,7 @@ Next we will add a newTestnetApp helper function: // newTestnetApp starts by running the normal newApp method. From there, the app interface returned is modified in order // for a testnet to be created from the provided app. func newTestnetApp(logger log.Logger, db cometbftdb.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application { - // Create an app and type cast to an MerlinApp + // Create an app and type cast to an simApp app := newApp(logger, db, traceStore, appOpts) simApp, ok := app.(*simapp.SimApp) if !ok { @@ -229,7 +229,7 @@ func newTestnetApp(logger log.Logger, db cometbftdb.DB, traceStore io.Writer, ap panic("upgradeToTrigger is not of type string") } - // Make modifications to the normal MerlinApp required to run the network locally + // Make modifications to the normal SimApp required to run the network locally return meriln.InitMerlinAppForTestnet(simApp, newValAddr, newValPubKey, newOperatorAddress, upgradeToTrigger) } ```