github.com/iotexproject/iotex-core@v1.14.1-rc1/ioctl/newcmd/node/nodedelegate.go (about)

     1  // Copyright (c) 2022 IoTeX
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package node
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"math/big"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
    16  	"github.com/iotexproject/iotex-proto/golang/iotexapi"
    17  	"github.com/pkg/errors"
    18  	"github.com/spf13/cobra"
    19  	"google.golang.org/grpc/codes"
    20  	"google.golang.org/grpc/status"
    21  
    22  	"github.com/iotexproject/iotex-core/ioctl"
    23  	"github.com/iotexproject/iotex-core/ioctl/config"
    24  	"github.com/iotexproject/iotex-core/ioctl/newcmd/bc"
    25  	"github.com/iotexproject/iotex-core/ioctl/util"
    26  	"github.com/iotexproject/iotex-core/state"
    27  )
    28  
    29  // Multi-language support
    30  var (
    31  	_delegateUses = map[config.Language]string{
    32  		config.English: "delegate [-e epoch-num|-n]",
    33  		config.Chinese: "delegate [-e epoch数|-n]",
    34  	}
    35  	_delegateShorts = map[config.Language]string{
    36  		config.English: "Print consensus delegates information in certain epoch",
    37  		config.Chinese: "打印在特定epoch内的共识委托信息",
    38  	}
    39  	_flagEpochNumUsages = map[config.Language]string{
    40  		config.English: "specify specific epoch",
    41  		config.Chinese: "指定特定epoch",
    42  	}
    43  	_flagNextEpochUsages = map[config.Language]string{
    44  		config.English: "query delegate of upcoming epoch",
    45  		config.Chinese: "查询即将到来的epoch的委托",
    46  	}
    47  )
    48  
    49  type nextDelegatesMessage struct {
    50  	Epoch      int        `json:"epoch"`
    51  	Determined bool       `json:"determined"`
    52  	Delegates  []delegate `json:"delegates"`
    53  }
    54  
    55  type delegate struct {
    56  	Address        string `json:"address"`
    57  	Rank           int    `json:"rank"`
    58  	Alias          string `json:"alias"`
    59  	Active         bool   `json:"active"`
    60  	Production     int    `json:"production"`
    61  	Votes          string `json:"votes"`
    62  	ProbatedStatus bool   `json:"_probatedStatus"`
    63  }
    64  
    65  type delegatesMessage struct {
    66  	Epoch       int        `json:"epoch"`
    67  	StartBlock  int        `json:"startBlock"`
    68  	TotalBlocks int        `json:"totalBlocks"`
    69  	Delegates   []delegate `json:"delegates"`
    70  }
    71  
    72  // NewNodeDelegateCmd represents the node delegate command
    73  func NewNodeDelegateCmd(client ioctl.Client) *cobra.Command {
    74  	var (
    75  		epochNum  uint64
    76  		nextEpoch bool
    77  	)
    78  
    79  	use, _ := client.SelectTranslation(_delegateUses)
    80  	short, _ := client.SelectTranslation(_delegateShorts)
    81  	flagEpochNumUsage, _ := client.SelectTranslation(_flagEpochNumUsages)
    82  	flagNextEpochUsage, _ := client.SelectTranslation(_flagNextEpochUsages)
    83  
    84  	cmd := &cobra.Command{
    85  		Use:   use,
    86  		Short: short,
    87  		Args:  cobra.ExactArgs(0),
    88  		RunE: func(cmd *cobra.Command, args []string) error {
    89  			cmd.SilenceUsage = true
    90  
    91  			if nextEpoch {
    92  				//nextDelegates
    93  				//deprecated: It won't be able to query next delegate after Easter height, because it will be determined at the end of the epoch.
    94  				currEpochNum, err := currEpochNum(client)
    95  				if err != nil {
    96  					return err
    97  				}
    98  				epochNum = currEpochNum + 1
    99  				message := nextDelegatesMessage{Epoch: int(epochNum)}
   100  
   101  				ctx := context.Background()
   102  				if jwtMD, err := util.JwtAuth(); err == nil {
   103  					ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx)
   104  				}
   105  				apiServiceClient, err := client.APIServiceClient()
   106  				if err != nil {
   107  					return err
   108  				}
   109  				abpResponse, err := apiServiceClient.ReadState(
   110  					ctx,
   111  					&iotexapi.ReadStateRequest{
   112  						ProtocolID: []byte("poll"),
   113  						MethodName: []byte("ActiveBlockProducersByEpoch"),
   114  						Arguments:  [][]byte{[]byte(strconv.FormatUint(epochNum, 10))},
   115  					},
   116  				)
   117  				if err != nil {
   118  					if sta, ok := status.FromError(err); ok {
   119  						if sta.Code() == codes.NotFound {
   120  							cmd.Println(message.String(epochNum))
   121  							return nil
   122  						} else if sta.Code() == codes.Unavailable {
   123  							return ioctl.ErrInvalidEndpointOrInsecure
   124  						}
   125  						return errors.New(sta.Message())
   126  					}
   127  					return errors.Wrap(err, "failed to invoke ReadState api")
   128  				}
   129  				message.Determined = true
   130  				var abps state.CandidateList
   131  				if err := abps.Deserialize(abpResponse.Data); err != nil {
   132  					return errors.Wrap(err, "failed to deserialize active bps")
   133  				}
   134  
   135  				bpResponse, err := apiServiceClient.ReadState(
   136  					ctx,
   137  					&iotexapi.ReadStateRequest{
   138  						ProtocolID: []byte("poll"),
   139  						MethodName: []byte("BlockProducersByEpoch"),
   140  						Arguments:  [][]byte{[]byte(strconv.FormatUint(epochNum, 10))},
   141  					},
   142  				)
   143  				if err != nil {
   144  					if sta, ok := status.FromError(err); ok {
   145  						if sta.Code() == codes.Unavailable {
   146  							return ioctl.ErrInvalidEndpointOrInsecure
   147  						}
   148  						return errors.New(sta.Message())
   149  					}
   150  					return errors.Wrap(err, "failed to invoke ReadState api")
   151  				}
   152  				var bps state.CandidateList
   153  				if err := bps.Deserialize(bpResponse.Data); err != nil {
   154  					return errors.Wrap(err, "failed to deserialize bps")
   155  				}
   156  
   157  				isActive := make(map[string]bool)
   158  				for _, abp := range abps {
   159  					isActive[abp.Address] = true
   160  				}
   161  				aliases := client.AliasMap()
   162  				for rank, bp := range bps {
   163  					votes := big.NewInt(0).SetBytes(bp.Votes.Bytes())
   164  					message.Delegates = append(message.Delegates, delegate{
   165  						Address: bp.Address,
   166  						Rank:    rank + 1,
   167  						Alias:   aliases[bp.Address],
   168  						Active:  isActive[bp.Address],
   169  						Votes:   util.RauToString(votes, util.IotxDecimalNum),
   170  					})
   171  				}
   172  				cmd.Println(message.String(epochNum))
   173  			} else {
   174  				// specfic epoch-num
   175  				if epochNum == 0 {
   176  					currEpochNum, err := currEpochNum(client)
   177  					if err != nil {
   178  						return err
   179  					}
   180  					epochNum = currEpochNum
   181  				}
   182  
   183  				response, err := bc.GetEpochMeta(client, epochNum)
   184  				if err != nil {
   185  					return errors.Wrap(err, "failed to get epoch meta")
   186  				}
   187  				if response.EpochData == nil {
   188  					return errors.New("rolldpos is not registered")
   189  				}
   190  				epochData := response.EpochData
   191  				aliases := client.AliasMap()
   192  				message := delegatesMessage{
   193  					Epoch:       int(epochData.Num),
   194  					StartBlock:  int(epochData.Height),
   195  					TotalBlocks: int(response.TotalBlocks),
   196  				}
   197  				probationList, err := getProbationList(client, epochNum, epochData.Height)
   198  				if err != nil {
   199  					return errors.Wrap(err, "failed to get probation list")
   200  				}
   201  				for rank, bp := range response.BlockProducersInfo {
   202  					votes, ok := new(big.Int).SetString(bp.Votes, 10)
   203  					if !ok {
   204  						return errors.New("failed to convert votes into big int")
   205  					}
   206  					isProbated := false
   207  					if _, ok := probationList.ProbationInfo[bp.Address]; ok {
   208  						// if it exists in probation list
   209  						isProbated = true
   210  					}
   211  					delegate := delegate{
   212  						Address:        bp.Address,
   213  						Rank:           rank + 1,
   214  						Alias:          aliases[bp.Address],
   215  						Active:         bp.Active,
   216  						Production:     int(bp.Production),
   217  						Votes:          util.RauToString(votes, util.IotxDecimalNum),
   218  						ProbatedStatus: isProbated,
   219  					}
   220  					message.Delegates = append(message.Delegates, delegate)
   221  				}
   222  				cmd.Println(message.String())
   223  			}
   224  			return nil
   225  		},
   226  	}
   227  
   228  	cmd.Flags().Uint64VarP(&epochNum, "epoch-num", "e", 0,
   229  		flagEpochNumUsage)
   230  	cmd.Flags().BoolVarP(&nextEpoch, "next-epoch", "n", false,
   231  		flagNextEpochUsage)
   232  	return cmd
   233  }
   234  
   235  func (m *nextDelegatesMessage) String(epochNum uint64) string {
   236  	if !m.Determined {
   237  		return fmt.Sprintf("delegates of upcoming epoch #%d are not determined", epochNum)
   238  	}
   239  	aliasLen := 5
   240  	for _, bp := range m.Delegates {
   241  		if len(bp.Alias) > aliasLen {
   242  			aliasLen = len(bp.Alias)
   243  		}
   244  	}
   245  	lines := []string{fmt.Sprintf("Epoch: %d\n", epochNum)}
   246  	formatTitleString := "%-41s   %-4s   %-" + strconv.Itoa(aliasLen) + "s   %-6s   %s"
   247  	formatDataString := "%-41s   %4d   %-" + strconv.Itoa(aliasLen) + "s   %-6s   %s"
   248  	lines = append(lines, fmt.Sprintf(formatTitleString, "Address", "Rank", "Alias", "Status", "Votes"))
   249  
   250  	var status string
   251  	for _, bp := range m.Delegates {
   252  		if bp.Active {
   253  			status = "active"
   254  		}
   255  		lines = append(lines, fmt.Sprintf(formatDataString, bp.Address, bp.Rank,
   256  			bp.Alias, status, bp.Votes))
   257  	}
   258  	return strings.Join(lines, "\n")
   259  }
   260  
   261  func (m *delegatesMessage) String() string {
   262  	aliasLen := 5
   263  	for _, bp := range m.Delegates {
   264  		if len(bp.Alias) > aliasLen {
   265  			aliasLen = len(bp.Alias)
   266  		}
   267  	}
   268  	lines := []string{fmt.Sprintf("Epoch: %d,  Start block height: %d,Total blocks in epoch: %d\n",
   269  		m.Epoch, m.StartBlock, m.TotalBlocks)}
   270  	formatTitleString := "%-41s   %-4s   %-" + strconv.Itoa(aliasLen) + "s   %-6s   %-6s   %-12s    %s"
   271  	formatDataString := "%-41s   %4d   %-" + strconv.Itoa(aliasLen) + "s   %-6s   %-6d   %-12s    %s"
   272  	lines = append(lines, fmt.Sprintf(formatTitleString,
   273  		"Address", "Rank", "Alias", "Status", "Blocks", "ProbatedStatus", "Votes"))
   274  
   275  	var (
   276  		status         string
   277  		probatedStatus string
   278  	)
   279  	for _, bp := range m.Delegates {
   280  		if bp.Active {
   281  			status = "active"
   282  		}
   283  		if bp.ProbatedStatus {
   284  			probatedStatus = "probated"
   285  		}
   286  		lines = append(lines, fmt.Sprintf(formatDataString, bp.Address, bp.Rank,
   287  			bp.Alias, status, bp.Production, probatedStatus, bp.Votes))
   288  	}
   289  	return strings.Join(lines, "\n")
   290  }
   291  
   292  func currEpochNum(client ioctl.Client) (uint64, error) {
   293  	chainMeta, err := bc.GetChainMeta(client)
   294  	if err != nil {
   295  		return 0, errors.Wrap(err, "failed to get chain meta")
   296  	}
   297  	epoch := chainMeta.Epoch
   298  	if epoch == nil {
   299  		return 0, errors.Wrap(err, "rolldpos is not registered")
   300  	}
   301  	return epoch.Num, nil
   302  }