diff --git a/cmd/atomoned/cmd/inplace_testnet.go b/cmd/atomoned/cmd/inplace_testnet.go new file mode 100644 index 000000000..a1d1c34be --- /dev/null +++ b/cmd/atomoned/cmd/inplace_testnet.go @@ -0,0 +1,245 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cast" + "github.com/spf13/cobra" + + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/libs/bytes" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + + dbm "github.com/cosmos/cosmos-db" + + "cosmossdk.io/log" + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + + "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" + + atomone "github.com/atomone-hub/atomone/app" + "github.com/atomone-hub/atomone/app/params" +) + +const valVotingPower int64 = 900000000000000 + +var flagAccountsToFund = "accounts-to-fund" + +type valArgs struct { + newValAddr bytes.HexBytes + newOperatorAddress string + newValPubKey crypto.PubKey + accountsToFund []string + upgradeToTrigger string + homeDir string +} + +func NewInPlaceTestnetCmd() *cobra.Command { + cmd := server.InPlaceTestnetCreator(newTestnetApp) + cmd.Example = `atomoned in-place-testnet testing-1 atonevaloper1w7f3xx7e75p4l7qdym5msqem9rd4dyc4jfa7ag --home $HOME/.atomone/validator1 --validator-privkey=6dq+/KHNvyiw2TToCgOpUpQKIzrLs69Rb8Az39xvmxPHNoPxY1Cil8FY+4DhT9YwD6s0tFABMlLcpaylzKKBOg== --accounts-to-fund="atone1f7twgcq4ypzg7y24wuywy06xmdet8pc4m7dv9c,atone1qvuhm5m644660nd8377d6l7yz9e9hhm9hv8p87"` + + 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.(*atomone.AtomOneApp) + if !ok { + panic("app created from newApp is not of type App") + } + + // Get command args + args, err := getCommandArgs(appOpts) + if err != nil { + panic(err) + } + + return initAppForTestnet(testApp, args) +} + +func initAppForTestnet(app *atomone.AtomOneApp, args valArgs) *atomone.AtomOneApp { + // Required Changes: + // + ctx := app.NewUncachedContext(true, cmtproto.Header{}) + + pubkey := &ed25519.PubKey{Key: args.newValPubKey.Bytes()} + pubkeyAny, err := codectypes.NewAnyWithValue(pubkey) + handleErr(err) + + // STAKING + // + + // Create Validator struct for our new validator. + newVal := stakingtypes.Validator{ + OperatorAddress: args.newOperatorAddress, + ConsensusPubkey: pubkeyAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(valVotingPower), + DelegatorShares: math.LegacyMustNewDecFromStr("10000000"), + Description: stakingtypes.Description{ + Moniker: "Testnet Validator", + }, + Commission: stakingtypes.Commission{ + CommissionRates: stakingtypes.CommissionRates{ + Rate: math.LegacyMustNewDecFromStr("0.05"), + MaxRate: math.LegacyMustNewDecFromStr("0.1"), + MaxChangeRate: math.LegacyMustNewDecFromStr("0.05"), + }, + }, + MinSelfDelegation: math.OneInt(), + } + + validator, err := app.StakingKeeper.ValidatorAddressCodec().StringToBytes(newVal.GetOperator()) + handleErr(err) + + // Remove all validators from power store + stakingKey := app.GetKey(stakingtypes.ModuleName) + stakingStore := ctx.KVStore(stakingKey) + iterator, err := app.StakingKeeper.ValidatorsPowerStoreIterator(ctx) + handleErr(err) + + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all validators from last validators store + iterator, err = app.StakingKeeper.LastValidatorsIterator(ctx) + handleErr(err) + + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all validators from validators store + iterator = stakingStore.Iterator(stakingtypes.ValidatorsKey, storetypes.PrefixEndBytes(stakingtypes.ValidatorsKey)) + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all validators from unbonding queue + iterator = stakingStore.Iterator(stakingtypes.ValidatorQueueKey, storetypes.PrefixEndBytes(stakingtypes.ValidatorQueueKey)) + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Add our validator to power and last validators store + handleErr(app.StakingKeeper.SetValidator(ctx, newVal)) + handleErr(app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal)) + handleErr(app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal)) + handleErr(app.StakingKeeper.SetLastValidatorPower(ctx, validator, 0)) + handleErr(app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, validator)) + + // DISTRIBUTION + // + + // Initialize records for this validator across all distribution stores + handleErr(app.DistrKeeper.SetValidatorHistoricalRewards(ctx, validator, 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1))) + handleErr(app.DistrKeeper.SetValidatorCurrentRewards(ctx, validator, distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1))) + handleErr(app.DistrKeeper.SetValidatorAccumulatedCommission(ctx, validator, distrtypes.InitialValidatorAccumulatedCommission())) + handleErr(app.DistrKeeper.SetValidatorOutstandingRewards(ctx, validator, distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}})) + + // 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, + } + _ = app.SlashingKeeper.SetValidatorSigningInfo(ctx, newConsAddr, newValidatorSigningInfo) + + // BANK + // + bondDenom, err := app.StakingKeeper.BondDenom(ctx) + handleErr(err) + if bondDenom == "" { + bondDenom = params.BondDenom + } + + defaultCoins := sdk.NewCoins(sdk.NewInt64Coin(bondDenom, 1000000000)) + + // Fund local accounts + for _, accountStr := range args.accountsToFund { + handleErr(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins)) + + account, err := app.AccountKeeper.AddressCodec().StringToBytes(accountStr) + handleErr(err) + + handleErr(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins)) + } + + return app +} + +// parse the input flags and returns valArgs +func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) { + args := valArgs{} + + newValAddr, ok := appOpts.Get(server.KeyNewValAddr).(bytes.HexBytes) + if !ok { + return args, errors.New("newValAddr is not of type bytes.HexBytes") + } + args.newValAddr = newValAddr + newValPubKey, ok := appOpts.Get(server.KeyUserPubKey).(crypto.PubKey) + if !ok { + return args, errors.New("newValPubKey is not of type crypto.PubKey") + } + args.newValPubKey = newValPubKey + newOperatorAddress, ok := appOpts.Get(server.KeyNewOpAddr).(string) + if !ok { + return args, errors.New("newOperatorAddress is not of type string") + } + args.newOperatorAddress = newOperatorAddress + upgradeToTrigger, ok := appOpts.Get(server.KeyTriggerTestnetUpgrade).(string) + if !ok { + return args, errors.New("upgradeToTrigger is not of type string") + } + args.upgradeToTrigger = upgradeToTrigger + + // parsing and set accounts to fund + accountsString := cast.ToString(appOpts.Get(flagAccountsToFund)) + if len(accountsString) > 0 { + args.accountsToFund = strings.Split(accountsString, ",") + } + + // home dir + homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) + if homeDir == "" { + return args, errors.New("invalid home dir") + } + args.homeDir = homeDir + + return args, nil +} + +// handleErr prints the error and exits the program if the error is not nil +func handleErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/cmd/atomoned/cmd/root.go b/cmd/atomoned/cmd/root.go index e80fe4c42..35eccda4e 100644 --- a/cmd/atomoned/cmd/root.go +++ b/cmd/atomoned/cmd/root.go @@ -146,6 +146,7 @@ func initRootCmd( ) { rootCmd.AddCommand( genutilcli.InitCmd(basicManager, atomone.DefaultNodeHome), + NewInPlaceTestnetCmd(), tmcli.NewCompletionCmd(rootCmd, true), NewTestnetCmd(basicManager, banktypes.GenesisBalancesIterator{}), addDebugCommands(debug.Cmd()),