Skip to content

Commit

Permalink
Implement in-place-testnet on version v0.5.0 (#628)
Browse files Browse the repository at this point in the history
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
v           ✰  Thanks for creating a PR! You're awesome! ✰
v Please note that maintainers will only review those PRs with a
completed PR template.
☺ > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >  -->

## Purpose of Changes and their Description

Port in-place-testnet implementation to current version on main (v0.5.0)
so we can use it to test the upgrade to version v0.6.0.

Cherry picked from this PR:
#612

## Link(s) to Ticket(s) or Issue(s) resolved by this PR


https://linear.app/alloralabs/issue/PROTO-2668/implement-in-place-testnet-on-version-v050

## Are these changes tested and documented?

- [x] If tested, please describe how. If not, why tests are not needed.
- [ ] If documented, please describe where. If not, describe why docs
are not needed.
- [ ] Added to `Unreleased` section of `CHANGELOG.md`?

Tested using:
https://www.notion.so/upshothq/Testing-upgrades-locally-or-on-devnet-11f34d40db4880abac30d507ed4ee2a6

## Still Left Todo

*Fill this out if this is a Draft PR so others can help.*
  • Loading branch information
spooktheducks authored Oct 17, 2024
2 parents c1585f9 + b7d1c39 commit e58f0d2
Show file tree
Hide file tree
Showing 10 changed files with 482 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ data_validator/
# test setups
test/testnet
test/devnet
test/localnet
test/stress/*.log

# coverage
Expand Down
6 changes: 6 additions & 0 deletions cmd/allorad/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
servertypes "github.com/cosmos/cosmos-sdk/server/types"
"github.com/cosmos/cosmos-sdk/types/module"
authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
"github.com/cosmos/cosmos-sdk/x/crisis"
genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli"

"github.com/allora-network/allora-chain/app"
Expand All @@ -34,6 +35,7 @@ func initRootCmd(rootCmd *cobra.Command, txConfig client.TxConfig, basicManager
confixcmd.ConfigCommand(),
pruning.Cmd(newApp, app.DefaultNodeHome),
snapshot.Cmd(newApp),
NewInPlaceTestnetCmd(addModuleInitFlags),
)

server.AddCommands(rootCmd, app.DefaultNodeHome, newApp, appExport, func(startCmd *cobra.Command) {})
Expand All @@ -48,6 +50,10 @@ func initRootCmd(rootCmd *cobra.Command, txConfig client.TxConfig, basicManager
)
}

func addModuleInitFlags(startCmd *cobra.Command) {
crisis.AddModuleInitFlags(startCmd)
}

func queryCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "query",
Expand Down
326 changes: 326 additions & 0 deletions cmd/allorad/cmd/testnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
package cmd

import (
"fmt"
"io"
"strings"
"time"

"cosmossdk.io/errors"
"cosmossdk.io/log"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/types/bech32"

storetypes "cosmossdk.io/store/types"
upgradetypes "cosmossdk.io/x/upgrade/types"
"github.com/cometbft/cometbft/crypto"
"github.com/cometbft/cometbft/libs/bytes"
tmos "github.com/cometbft/cometbft/libs/os"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/server"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
sdk "github.com/cosmos/cosmos-sdk/types"
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/spf13/cast"
"github.com/spf13/cobra"

"github.com/allora-network/allora-chain/app"
)

var (
flagAccountsToFund = "accounts-to-fund"
)

type valArgs struct {
newValAddr bytes.HexBytes
newOperatorAddress string
newValPubKey crypto.PubKey
accountsToFund []sdk.AccAddress
upgradeToTrigger string
homeDir string
}

func NewInPlaceTestnetCmd(addStartFlags servertypes.ModuleInitFlags) *cobra.Command {
cmd := server.InPlaceTestnetCreator(newTestnetApp)
addStartFlags(cmd)
cmd.Short = "Updates chain's application and consensus state with provided validator info and starts the node"
cmd.Long = `The test command modifies both application and consensus stores within a local mainnet node and starts the node,
with the aim of facilitating testing procedures. This command replaces existing validator data with updated information,
thereby removing the old validator set and introducing a new set suitable for local testing purposes. By altering the state extracted from the mainnet node,
it enables developers to configure their local environments to reflect mainnet conditions more accurately.`

cmd.Example = fmt.Sprintf(`%sd in-place-testnet testing-1 allovaloper1xepena2sx9yzdwjtln0c5ycznw8vvzmt7ktnk5 --home $HOME/.%sd/data --accounts-to-fund="allo1xepena2sx9yzdwjtln0c5ycznw8vvzmtfjds2c"`, "allora", "allora")

cmd.Flags().String(flagAccountsToFund, "", "Comma-separated list of account addresses that will be funded for testing purposes")
return cmd
}

// 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 dbm.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application {
// Create an app and type cast to an App
newApp := newApp(logger, db, traceStore, appOpts)
testApp, ok := newApp.(*app.AlloraApp)
if !ok {
panic("app created from newApp is not of type App")
}

// Get command args
args, err := getCommandArgs(appOpts)
if err != nil {
panic(errors.Wrap(err, "newTestnetApp(): failed to get command args"))
}

return initAppForTestnet(testApp, args)
}

func initAppForTestnet(app *app.AlloraApp, args valArgs) *app.AlloraApp {
// Required Changes:
ctx := app.BaseApp.NewUncachedContext(true, tmproto.Header{}) //nolint:exhaustruct // cosmos-sdk logic

pubkey := &ed25519.PubKey{Key: args.newValPubKey.Bytes()}
pubkeyAny, err := codectypes.NewAnyWithValue(pubkey)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to create pubkey any").Error())
}

// STAKING
//

// Create Validator struct for our new validator.
_, bz, err := bech32.DecodeAndConvert(args.newOperatorAddress)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to decode operator address").Error())
}
bech32Addr, err := bech32.ConvertAndEncode("allovaloper", bz)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to convert and encode operator address").Error())
}
newVal := stakingtypes.Validator{
OperatorAddress: bech32Addr,
ConsensusPubkey: pubkeyAny,
Jailed: false,
Status: stakingtypes.Bonded,
Tokens: math.NewInt(9e18),
DelegatorShares: math.LegacyNewDec(1e18),
Description: stakingtypes.Description{
Moniker: "Testnet Validator",
Identity: "",
Website: "",
SecurityContact: "",
Details: "",
},
Commission: stakingtypes.Commission{
CommissionRates: stakingtypes.CommissionRates{
Rate: math.LegacyMustNewDecFromStr("0.05"),
MaxRate: math.LegacyMustNewDecFromStr("0.1"),
MaxChangeRate: math.LegacyMustNewDecFromStr("0.05"),
},
UpdateTime: time.Time{},
},
MinSelfDelegation: math.OneInt(),
UnbondingHeight: 0,
UnbondingTime: time.Time{},
UnbondingOnHoldRefCount: 0,
UnbondingIds: []uint64{},
}

// Remove all validators from power store
stakingKey := app.GetKey(stakingtypes.ModuleName)
stakingStore := ctx.KVStore(stakingKey)
validatorsPowerStoreIterator, err := app.StakingKeeper.ValidatorsPowerStoreIterator(ctx)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to get validators power store iterator").Error())
}
defer validatorsPowerStoreIterator.Close()
for ; validatorsPowerStoreIterator.Valid(); validatorsPowerStoreIterator.Next() {
stakingStore.Delete(validatorsPowerStoreIterator.Key())
}
validatorsPowerStoreIterator.Close()

// Remove all valdiators from last validators store
lastValidatorsIterator, err := app.StakingKeeper.LastValidatorsIterator(ctx)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to get last validators iterator").Error())
}
defer lastValidatorsIterator.Close()
for ; lastValidatorsIterator.Valid(); lastValidatorsIterator.Next() {
stakingStore.Delete(lastValidatorsIterator.Key())
}
lastValidatorsIterator.Close()

// Remove all validators from validators store
validatorsIterator := storetypes.KVStorePrefixIterator(stakingStore, stakingtypes.ValidatorsKey)
defer validatorsIterator.Close()
for ; validatorsIterator.Valid(); validatorsIterator.Next() {
stakingStore.Delete(validatorsIterator.Key())
}
validatorsIterator.Close()

// Remove all validators from unbonding queue
validatorQueueIterator := storetypes.KVStorePrefixIterator(stakingStore, stakingtypes.ValidatorQueueKey)
defer validatorQueueIterator.Close()
for ; validatorQueueIterator.Valid(); validatorQueueIterator.Next() {
stakingStore.Delete(validatorQueueIterator.Key())
}
validatorQueueIterator.Close()

// Add our validator to power and last validators store
err = app.StakingKeeper.SetValidator(ctx, newVal)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator").Error())
}
err = app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator by consensus address").Error())
}
err = app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator by power index").Error())
}
valAddr, err := sdk.ValAddressFromBech32(newVal.GetOperator())
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to convert validator address to bytes").Error())
}
err = app.StakingKeeper.SetLastValidatorPower(ctx, valAddr, 9e18)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set last validator power").Error())
}
err = app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, valAddr)
if err != nil {
panic(errors.Wrap(err, "initAppForTestnet(): failed to call after validator created hook").Error())
}

// DISTRIBUTION
//

valAddr, err = sdk.ValAddressFromBech32(newVal.GetOperator())
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to convert validator address to bytes").Error())
}
// Initialize records for this validator across all distribution stores
err = app.DistrKeeper.SetValidatorHistoricalRewards(ctx, valAddr, 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1))
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator historical rewards").Error())
}
err = app.DistrKeeper.SetValidatorCurrentRewards(ctx, valAddr, distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1))
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator current rewards").Error())
}
err = app.DistrKeeper.SetValidatorAccumulatedCommission(ctx, valAddr, distrtypes.InitialValidatorAccumulatedCommission())
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator accumulated commission").Error())
}
err = app.DistrKeeper.SetValidatorOutstandingRewards(ctx, valAddr, distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}})
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator outstanding rewards").Error())
}

// SLASHING
//

// Set validator signing info for our new validator.
newConsAddr := sdk.ConsAddress(args.newValAddr.Bytes())
newValidatorSigningInfo := slashingtypes.ValidatorSigningInfo{
Address: newConsAddr.String(),
StartHeight: app.LastBlockHeight() - 1,
Tombstoned: false,
IndexOffset: 0,
JailedUntil: time.Time{},
MissedBlocksCounter: 0,
}
err = app.SlashingKeeper.SetValidatorSigningInfo(ctx, newConsAddr, newValidatorSigningInfo)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to set validator signing info").Error())
}

// BANK
//
bondDenom, err := app.StakingKeeper.BondDenom(ctx)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to get bond denom").Error())
}

defaultCoins := sdk.NewCoins(sdk.NewInt64Coin(bondDenom, 9e18))

// Fund local accounts
for _, account := range args.accountsToFund {
err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to mint coins").Error())
}
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to send coins from module to account").Error())
}
}

if args.upgradeToTrigger != "" {
upgradePlan := upgradetypes.Plan{
Name: args.upgradeToTrigger,
Height: app.LastBlockHeight() + 10,
}
err = app.UpgradeKeeper.ScheduleUpgrade(ctx, upgradePlan)
if err != nil {
tmos.Exit(errors.Wrap(err, "initAppForTestnet(): failed to schedule upgrade").Error())
}
}

return app
}

// parse the input flags and returns valArgs
func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) {
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")
}

// validate and set accounts to fund
accountsString := cast.ToString(appOpts.Get(flagAccountsToFund))

accountsToFund := []sdk.AccAddress{}
for _, account := range strings.Split(accountsString, ",") {
if account != "" {
addr, err := sdk.AccAddressFromBech32(account)
if err != nil {
return valArgs{}, fmt.Errorf("invalid bech32 address format %w", err)
}
accountsToFund = append(accountsToFund, addr)
}
}

// home dir
homeDir := cast.ToString(appOpts.Get(flags.FlagHome))
if homeDir == "" {
return valArgs{}, fmt.Errorf("invalid home dir")
}

return valArgs{
newValAddr: newValAddr,
newOperatorAddress: newOperatorAddress,
newValPubKey: newValPubKey,
accountsToFund: accountsToFund,
upgradeToTrigger: upgradeToTrigger,
homeDir: homeDir,
}, nil
}
Loading

0 comments on commit e58f0d2

Please sign in to comment.