Skip to content
This repository has been archived by the owner on Feb 21, 2023. It is now read-only.

Add remove subnet support #44

Merged
merged 5 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions client/p.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ type P interface {
weight uint64,
opts ...OpOption,
) (took time.Duration, err error)
RemoveSubnetValidator(
ctx context.Context,
k key.Key,
subnetID ids.ID,
nodeID ids.NodeID,
opts ...OpOption,
) (took time.Duration, err error)
CreateBlockchain(
ctx context.Context,
key key.Key,
Expand Down Expand Up @@ -319,6 +326,90 @@ func (pc *p) AddSubnetValidator(
return pc.checker.PollTx(ctx, txID, pstatus.Committed)
}

// ref. "platformvm.VM.newRemoveSubnetValidatorTx".
func (pc *p) RemoveSubnetValidator(
ctx context.Context,
k key.Key,
subnetID ids.ID,
nodeID ids.NodeID,
opts ...OpOption,
) (took time.Duration, err error) {
ret := &Op{}
ret.applyOpts(opts)

if subnetID == ids.Empty {
// same as "ErrNamedSubnetCantBePrimary"
// in case "subnetID == constants.PrimaryNetworkID"
return 0, ErrEmptyID
}
if nodeID == ids.EmptyNodeID {
return 0, ErrEmptyID
}

_, validateEnd, err := pc.GetValidator(ctx, subnetID, nodeID)
if errors.Is(err, ErrValidatorNotFound) {
return 0, ErrValidatorNotFound
} else if err != nil {
return 0, fmt.Errorf("%w: unable to get subnet validator record", err)
}
// make sure the range is within staker validation start/end on the subnet
now := time.Now()
// We don't check [validateStart] because we can remove pending validators.
if now.After(validateEnd) {
return 0, fmt.Errorf("%w (validate end %v expected <%v)", ErrInvalidSubnetValidatePeriod, now, validateEnd)
}

fi, err := pc.info.GetTxFee(ctx)
if err != nil {
return 0, err
}
txFee := uint64(fi.TxFee)

zap.L().Info("removing subnet validator",
zap.String("subnetId", subnetID.String()),
zap.Uint64("txFee", txFee),
)
ins, returnedOuts, _, signers, err := pc.stake(ctx, k, txFee)
if err != nil {
return 0, err
}
subnetAuth, subnetSigners, err := pc.authorize(ctx, k, subnetID)
if err != nil {
return 0, err
}
signers = append(signers, subnetSigners)

utx := &txs.RemoveSubnetValidatorTx{
BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
NetworkID: pc.networkID,
BlockchainID: pc.pChainID,
Ins: ins,
Outs: returnedOuts,
}},
NodeID: nodeID,
Subnet: subnetID,
SubnetAuth: subnetAuth,
}
pTx := &txs.Tx{
Unsigned: utx,
}
if err := k.Sign(pTx, signers); err != nil {
return 0, err
}
if err := utx.SyntacticVerify(&snow.Context{
NetworkID: pc.networkID,
ChainID: pc.pChainID,
}); err != nil {
return 0, err
}
txID, err := pc.cli.IssueTx(ctx, pTx.Bytes())
if err != nil {
return 0, fmt.Errorf("failed to issue tx: %w", err)
}

return pc.checker.PollTx(ctx, txID, pstatus.Committed)
}

// ref. "platformvm.VM.newAddValidatorTx".
func (pc *p) AddValidator(
ctx context.Context,
Expand Down
2 changes: 1 addition & 1 deletion cmd/add_subnet_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func createSubnetValidatorFunc(cmd *cobra.Command, args []string) error {
return err
}
info.txFee = uint64(info.feeData.TxFee)
if err := ParseNodeIDs(cli, info); err != nil {
if err := ParseNodeIDs(cli, info, true); err != nil {
return err
}
if len(info.nodeIDs) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion cmd/add_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func createValidatorFunc(cmd *cobra.Command, args []string) error {
info.stakeAmount = stakeAmount

info.subnetID = ids.Empty
if err := ParseNodeIDs(cli, info); err != nil {
if err := ParseNodeIDs(cli, info, true); err != nil {
return err
}
if len(info.nodeIDs) == 0 {
Expand Down
25 changes: 21 additions & 4 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
)

const (
Version = "0.0.3"
Version = "0.0.4"
)

type ValInfo struct {
Expand Down Expand Up @@ -176,7 +176,7 @@ func BaseTableSetup(i *Info) (*bytes.Buffer, *tablewriter.Table) {
return buf, tb
}

func ParseNodeIDs(cli client.Client, i *Info) error {
func ParseNodeIDs(cli client.Client, i *Info, add bool) error {
// TODO: make this parsing logic more explicit (+ store per subnetID, not
// just whatever was called last)
i.nodeIDs = []ids.NodeID{}
Expand All @@ -191,11 +191,15 @@ func ParseNodeIDs(cli client.Client, i *Info) error {
start, end, err := cli.P().GetValidator(context.Background(), i.subnetID, nodeID)
i.valInfos[nodeID] = &ValInfo{start, end}
switch {
case errors.Is(err, client.ErrValidatorNotFound):
case add && errors.Is(err, client.ErrValidatorNotFound):
i.nodeIDs = append(i.nodeIDs, nodeID)
case !add && err == nil:
i.nodeIDs = append(i.nodeIDs, nodeID)
case !add && errors.Is(err, client.ErrValidatorNotFound):
color.Outf("\n{{yellow}}%s is not yet a validator on %s{{/}}\n", nodeID, i.subnetID)
case err != nil:
return err
default:
case add:
color.Outf("\n{{yellow}}%s is already a validator on %s{{/}}\n", nodeID, i.subnetID)
}
}
Expand All @@ -217,3 +221,16 @@ func WaitValidator(cli client.Client, nodeIDs []ids.NodeID, i *Info) {
}
}
}

func WaitValidatorRemoval(cli client.Client, nodeIDs []ids.NodeID, i *Info) {
for _, nodeID := range nodeIDs {
color.Outf("{{yellow}}waiting for validator %s to stop validating %s...(could take a few minutes){{/}}\n", nodeID, i.subnetID)
for {
_, _, err := cli.P().GetValidator(context.Background(), i.subnetID, nodeID)
if errors.Is(err, client.ErrValidatorNotFound) {
break
}
time.Sleep(10 * time.Second)
}
}
}
41 changes: 41 additions & 0 deletions cmd/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package cmd

import (
"github.com/ava-labs/avalanchego/ids"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/spf13/cobra"
)

// RemoveCommand implements "subnet-cli remove" command.
func RemoveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Sub-commands for removing resources",
}
cmd.AddCommand(
newRemoveSubnetValidatorCommand(),
)
cmd.PersistentFlags().StringVar(&publicURI, "public-uri", "https://api.avax-test.network", "URI for avalanche network endpoints")
cmd.PersistentFlags().StringVar(&privKeyPath, "private-key-path", ".subnet-cli.pk", "private key file path")
cmd.PersistentFlags().BoolVarP(&useLedger, "ledger", "l", false, "use ledger to sign transactions")
return cmd
}

func CreateRemoveValidator(i *Info) string {
buf, tb := BaseTableSetup(i)
tb.Append([]string{formatter.F("{{orange}}NODE IDs{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.nodeIDs)})
if i.subnetID != ids.Empty {
tb.Append([]string{formatter.F("{{blue}}SUBNET ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.subnetID)})
}
if i.rewardAddr != ids.ShortEmpty {
tb.Append([]string{formatter.F("{{cyan}}{{bold}}REWARD ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.rewardAddr)})
}
if i.changeAddr != ids.ShortEmpty {
tb.Append([]string{formatter.F("{{cyan}}{{bold}}CHANGE ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.changeAddr)})
}
tb.Render()
return buf.String()
}
118 changes: 118 additions & 0 deletions cmd/remove_subnet_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package cmd

import (
"context"
"fmt"
"os"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/subnet-cli/pkg/color"
"github.com/manifoldco/promptui"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/spf13/cobra"
)

func newRemoveSubnetValidatorCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "subnet-validator",
Short: "Removes the validator from a subnet",
Long: `
Removes a subnet validator.

$ subnet-cli remove subnet-validator \
--private-key-path=.insecure.ewoq.key \
--public-uri=http://localhost:52250 \
--subnet-id="24tZhrm8j8GCJRE9PomW8FaeqbgGS4UAQjJnqqn8pq5NwYSYV1" \
--node-ids="NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH"
`,
RunE: removeSubnetValidatorFunc,
}

cmd.PersistentFlags().StringVar(&subnetIDs, "subnet-id", "", "subnet ID (must be formatted in ids.ID)")
cmd.PersistentFlags().StringSliceVar(&nodeIDs, "node-ids", nil, "a list of node IDs (must be formatted in ids.ID)")

return cmd
}

func removeSubnetValidatorFunc(cmd *cobra.Command, args []string) error {
cli, info, err := InitClient(publicURI, true)
if err != nil {
return err
}
info.subnetID, err = ids.FromString(subnetIDs)
if err != nil {
return err
}
info.txFee = uint64(info.feeData.TxFee)
if err := ParseNodeIDs(cli, info, false); err != nil {
return err
}
if len(info.nodeIDs) == 0 {
color.Outf("{{magenta}}no subnet validators to add{{/}}\n")
return nil
}
info.txFee *= uint64(len(info.nodeIDs))
info.requiredBalance = info.txFee
if err := info.CheckBalance(); err != nil {
return err
}
msg := CreateRemoveValidator(info)
if enablePrompt {
msg = formatter.F("\n{{blue}}{{bold}}Ready to remove subnet validator, should we continue?{{/}}\n") + msg
}
fmt.Fprint(formatter.ColorableStdOut, msg)

if enablePrompt {
prompt := promptui.Select{
Label: "\n",
Stdout: os.Stdout,
Items: []string{
formatter.F("{{green}}Yes, let's remove! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"),
formatter.F("{{red}}No, stop it!{{/}}"),
},
}
idx, _, err := prompt.Run()
if err != nil {
return nil //nolint:nilerr
}
if idx == 1 {
return nil
}
}

println()
println()
println()
for _, nodeID := range info.nodeIDs {
// valInfo is not populated because [ParseNodeIDs] called on info.subnetID
//
// TODO: cleanup
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
took, err := cli.P().RemoveSubnetValidator(
ctx,
info.key,
info.subnetID,
nodeID,
)
cancel()
if err != nil {
return err
}
color.Outf("{{magenta}}removed %s from subnet %s validator set{{/}} {{light-gray}}(took %v){{/}}\n\n", nodeID, info.subnetID, took)
}
WaitValidatorRemoval(cli, info.nodeIDs, info)
info.requiredBalance = 0
info.stakeAmount = 0
info.txFee = 0
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
info.balance, err = cli.P().Balance(ctx, info.key)
cancel()
if err != nil {
return err
}
fmt.Fprint(formatter.ColorableStdOut, CreateAddTable(info))
return nil
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func init() {
rootCmd.AddCommand(
CreateCommand(),
AddCommand(),
RemoveCommand(),
StatusCommand(),
WizardCommand(),
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func wizardFunc(cmd *cobra.Command, args []string) error {

// Parse Args
info.subnetID = ids.Empty
if err := ParseNodeIDs(cli, info); err != nil {
if err := ParseNodeIDs(cli, info, true); err != nil {
return err
}
info.stakeAmount = stakeAmount
Expand Down