diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go index 36ed55fdbb..00b9eb1fa1 100644 --- a/x/gov/client/cli/tx.go +++ b/x/gov/client/cli/tx.go @@ -132,7 +132,7 @@ metadata example: return err } - msgs, metadata, title, summary, deposit, err := parseSubmitProposal(clientCtx.Codec, args[0]) + msgs, metadata, title, summary, deposit, err := ParseSubmitProposal(clientCtx.Codec, args[0]) if err != nil { return err } diff --git a/x/gov/client/cli/util.go b/x/gov/client/cli/util.go index fe3e8d0a26..4985e97a77 100644 --- a/x/gov/client/cli/util.go +++ b/x/gov/client/cli/util.go @@ -89,8 +89,8 @@ type proposal struct { Summary string `json:"summary"` } -// parseSubmitProposal reads and parses the proposal. -func parseSubmitProposal(cdc codec.Codec, path string) ([]sdk.Msg, string, string, string, sdk.Coins, error) { +// ParseSubmitProposal reads and parses the proposal. +func ParseSubmitProposal(cdc codec.Codec, path string) ([]sdk.Msg, string, string, string, sdk.Coins, error) { var proposal proposal contents, err := os.ReadFile(path) diff --git a/x/gov/client/cli/util_test.go b/x/gov/client/cli/util_test.go index 7abfd86c0e..d4cca03c57 100644 --- a/x/gov/client/cli/util_test.go +++ b/x/gov/client/cli/util_test.go @@ -170,15 +170,15 @@ func TestParseSubmitProposal(t *testing.T) { badJSON := testutil.WriteToNewTempFile(t, "bad json") // nonexistent json - _, _, _, _, _, err := parseSubmitProposal(cdc, "fileDoesNotExist") //nolint: dogsled + _, _, _, _, _, err := ParseSubmitProposal(cdc, "fileDoesNotExist") //nolint: dogsled require.Error(t, err) // invalid json - _, _, _, _, _, err = parseSubmitProposal(cdc, badJSON.Name()) //nolint: dogsled + _, _, _, _, _, err = ParseSubmitProposal(cdc, badJSON.Name()) //nolint: dogsled require.Error(t, err) // ok json - msgs, metadata, title, summary, deposit, err := parseSubmitProposal(cdc, okJSON.Name()) + msgs, metadata, title, summary, deposit, err := ParseSubmitProposal(cdc, okJSON.Name()) require.NoError(t, err, "unexpected error") require.Equal(t, sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000))), deposit) require.Equal(t, base64.StdEncoding.EncodeToString(expectedMetadata), metadata) diff --git a/x/staking/client/cli/tx.go b/x/staking/client/cli/tx.go index 5a43d79214..16daec6e56 100644 --- a/x/staking/client/cli/tx.go +++ b/x/staking/client/cli/tx.go @@ -2,6 +2,11 @@ package cli import ( "fmt" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" "os" "strconv" "strings" @@ -42,6 +47,7 @@ func NewTxCmd() *cobra.Command { } stakingTxCmd.AddCommand( + NewCreateValidatorCmd(), NewEditValidatorCmd(), NewDelegateCmd(), @@ -52,6 +58,116 @@ func NewTxCmd() *cobra.Command { return stakingTxCmd } +// NewCreateValidatorCmd returns a CLI command handler for creating a MsgCreateValidator transaction. +func NewCreateValidatorCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-validator [path/to/create_validator_proposal.json]", + Short: "submit a create new validator proposal", + Args: cobra.ExactArgs(1), + Long: `Submit a create new validator proposal by submitting a JSON file with the new validator details, once the proposal has been passed, create a new validator initialized with a self-delegation.`, + Example: strings.TrimSpace( + fmt.Sprintf(` +$ %s tx staking create-validator path/to/create_validator_proposal.json --from keyname + +Where create_validator_proposal.json contains: + +{ + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "${NODE_NAME}", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.070000000000000000", + "max_rate": "1.000000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1000000000000000000000", + "delegator_address": "${VALIDATOR_ADDR}", + "validator_address": "${VALIDATOR_ADDR}", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "${VALIDATOR_NODE_PUB_KEY}" + }, + "value": { + "denom": "BNB", + "amount": "1000000000000000000000" + }, + "from": "0x7b5Fe22B5446f7C62Ea27B8BD71CeF94e03f3dF2", + "relayer_address": "${RELAYER_ADDR}", + "challenger_address": "${CHALLENGER_ADDR}", + "bls_key": "${VALIDATOR_BLS}" + } + ], + "metadata": "", + "title": "Create ${NODE_NAME} Validator", + "summary": "create ${NODE_NAME} validator", + "deposit": "1000000000000000000BNB" +} + +modify the related configrations as you need, where you can get the pubkey using "%s tendermint show-validator" +`, version.AppName, version.AppName)), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msgs, metadata, title, summary, deposit, err := govcli.ParseSubmitProposal(clientCtx.Codec, args[0]) + if err != nil { + return err + } + + govMsg, err := v1.NewMsgSubmitProposal(msgs, deposit, clientCtx.GetFromAddress().String(), metadata, title, summary) + if err != nil { + return fmt.Errorf("invalid message: %w", err) + } + + if len(msgs) != 1 { + return fmt.Errorf("invalid message length: %d", len(msgs)) + } + + valMsg, ok := msgs[0].(*types.MsgCreateValidator) + if !ok || valMsg.ValidateBasic() != nil { + return fmt.Errorf("invalid create validator message") + } + + delAddr, err := sdk.AccAddressFromHexUnsafe(valMsg.DelegatorAddress) + if err != nil { + return err + } + if !delAddr.Equals(clientCtx.GetFromAddress()) { + return fmt.Errorf("the from address should be the self delegator address: %s", delAddr.String()) + } + + valAddr, err := sdk.AccAddressFromHexUnsafe(valMsg.ValidatorAddress) + if err != nil { + return err + } + + grantee := authtypes.NewModuleAddress(govtypes.ModuleName) + authorization, err := types.NewStakeAuthorization([]sdk.AccAddress{valAddr}, nil, types.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &valMsg.Value) + authzMsg, err := authz.NewMsgGrant(clientCtx.GetFromAddress(), grantee, authorization, nil) + if err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), authzMsg, govMsg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + _ = cmd.MarkFlagRequired(flags.FlagFrom) + + return cmd +} + // NewEditValidatorCmd returns a CLI command handler for creating a MsgEditValidator transaction. func NewEditValidatorCmd() *cobra.Command { cmd := &cobra.Command{ diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index 45f9044b8e..3ded029ffe 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -84,17 +84,14 @@ func (msg MsgCreateValidator) GetSignBytes() []byte { // ValidateBasic implements the sdk.Msg interface. func (msg MsgCreateValidator) ValidateBasic() error { // note that unmarshaling from bech32 ensures both non-empty and valid - delAddr, err := sdk.AccAddressFromHexUnsafe(msg.DelegatorAddress) + _, err := sdk.AccAddressFromHexUnsafe(msg.DelegatorAddress) if err != nil { return sdkerrors.ErrInvalidAddress.Wrapf("invalid delegator address: %s", err) } - valAddr, err := sdk.AccAddressFromHexUnsafe(msg.ValidatorAddress) + _, err = sdk.AccAddressFromHexUnsafe(msg.ValidatorAddress) if err != nil { return sdkerrors.ErrInvalidAddress.Wrapf("invalid validator address: %s", err) } - if !sdk.AccAddress(valAddr).Equals(delAddr) { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "validator address is invalid") - } if msg.Pubkey == nil { return ErrEmptyValidatorPubKey