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

     1  // Copyright (c) 2022 IoTeX Foundation
     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  	"sort"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
    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-proto/golang/iotexapi"
    23  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    24  
    25  	"github.com/iotexproject/iotex-core/action/protocol/vote"
    26  	"github.com/iotexproject/iotex-core/ioctl/cmd/alias"
    27  	"github.com/iotexproject/iotex-core/ioctl/cmd/bc"
    28  	"github.com/iotexproject/iotex-core/ioctl/config"
    29  	"github.com/iotexproject/iotex-core/ioctl/output"
    30  	"github.com/iotexproject/iotex-core/ioctl/util"
    31  	"github.com/iotexproject/iotex-core/state"
    32  )
    33  
    34  const (
    35  	_protocolID          = "staking"
    36  	_readCandidatesLimit = 20000
    37  	_defaultDelegateNum  = 36
    38  )
    39  
    40  // Multi-language support
    41  var (
    42  	_delegateCmdUses = map[config.Language]string{
    43  		config.English: "delegate [-e epoch-num] [-a]",
    44  		config.Chinese: "delegate [-e epoch数] [-a]",
    45  	}
    46  	_delegateCmdShorts = map[config.Language]string{
    47  		config.English: "Print consensus delegates information in certain epoch",
    48  		config.Chinese: "打印在特定epoch内的共识代表的信息",
    49  	}
    50  	_flagEpochNumUsages = map[config.Language]string{
    51  		config.English: "specify specific epoch",
    52  		config.Chinese: "指定特定epoch",
    53  	}
    54  )
    55  
    56  var (
    57  	_epochNum       uint64
    58  	_nodeStatus     map[bool]string
    59  	_probatedStatus map[bool]string
    60  )
    61  
    62  // _nodeDelegateCmd represents the node delegate command
    63  var _nodeDelegateCmd = &cobra.Command{
    64  	Use:   config.TranslateInLang(_delegateCmdUses, config.UILanguage),
    65  	Short: config.TranslateInLang(_delegateCmdShorts, config.UILanguage),
    66  	Args:  cobra.ExactArgs(0),
    67  	RunE: func(cmd *cobra.Command, args []string) error {
    68  		cmd.SilenceUsage = true
    69  		err := delegates()
    70  		return output.PrintError(err)
    71  	},
    72  }
    73  
    74  type delegate struct {
    75  	Address            string   `json:"address"`
    76  	Name               string   `json:"string"`
    77  	Rank               int      `json:"rank"`
    78  	Alias              string   `json:"alias"`
    79  	Active             bool     `json:"active"`
    80  	Production         int      `json:"production"`
    81  	Votes              string   `json:"votes"`
    82  	ProbatedStatus     bool     `json:"_probatedStatus"`
    83  	TotalWeightedVotes *big.Int `json:"totalWeightedVotes"`
    84  }
    85  
    86  type delegatesMessage struct {
    87  	Epoch       int        `json:"epoch"`
    88  	StartBlock  int        `json:"startBlock"`
    89  	TotalBlocks int        `json:"totalBlocks"`
    90  	Delegates   []delegate `json:"delegates"`
    91  }
    92  
    93  func (m *delegatesMessage) String() string {
    94  	if output.Format == "" {
    95  		aliasLen := 5
    96  		for _, bp := range m.Delegates {
    97  			if len(bp.Alias) > aliasLen {
    98  				aliasLen = len(bp.Alias)
    99  			}
   100  		}
   101  		lines := []string{fmt.Sprintf("Epoch: %d,  Start block height: %d,Total blocks produced in epoch: %d\n",
   102  			m.Epoch, m.StartBlock, m.TotalBlocks)}
   103  		formatTitleString := "%-41s   %-12s   %-4s   %-" + strconv.Itoa(aliasLen) + "s   %-6s   %-6s   %-12s    %s"
   104  		formatDataString := "%-41s   %-12s   %4d   %-" + strconv.Itoa(aliasLen) + "s   %-6s   %-6d   %-12s    %s"
   105  		lines = append(lines, fmt.Sprintf(formatTitleString,
   106  			"Address", "Name", "Rank", "Alias", "Status", "Blocks", "ProbatedStatus", "Votes"))
   107  		for _, bp := range m.Delegates {
   108  			lines = append(lines, fmt.Sprintf(formatDataString, bp.Address, bp.Name, bp.Rank, bp.Alias, _nodeStatus[bp.Active], bp.Production, _probatedStatus[bp.ProbatedStatus], bp.Votes))
   109  		}
   110  		return strings.Join(lines, "\n")
   111  	}
   112  	return output.FormatString(output.Result, m)
   113  }
   114  
   115  func init() {
   116  	_nodeDelegateCmd.Flags().Uint64VarP(&_epochNum, "epoch-num", "e", 0,
   117  		config.TranslateInLang(_flagEpochNumUsages, config.UILanguage))
   118  	_nodeStatus = map[bool]string{true: "active", false: ""}
   119  	_probatedStatus = map[bool]string{true: "probated", false: ""}
   120  }
   121  
   122  func delegates() error {
   123  	if _epochNum == 0 {
   124  		chainMeta, err := bc.GetChainMeta()
   125  		if err != nil {
   126  			return output.NewError(0, "failed to get chain meta", err)
   127  		}
   128  		epochData := chainMeta.GetEpoch()
   129  		if epochData == nil {
   130  			return output.NewError(0, "ROLLDPOS is not registered", nil)
   131  		}
   132  		_epochNum = epochData.Num
   133  	}
   134  	response, err := bc.GetEpochMeta(_epochNum)
   135  	if err != nil {
   136  		return output.NewError(0, "failed to get epoch meta", err)
   137  	}
   138  	if response.EpochData == nil {
   139  		return output.NewError(0, "ROLLDPOS is not registered", nil)
   140  	}
   141  	epochData := response.EpochData
   142  	aliases := alias.GetAliasMap()
   143  	message := delegatesMessage{
   144  		Epoch:       int(epochData.Num),
   145  		StartBlock:  int(epochData.Height),
   146  		TotalBlocks: int(response.TotalBlocks),
   147  	}
   148  	probationList, err := getProbationList(_epochNum, epochData.Height)
   149  	if err != nil {
   150  		return output.NewError(0, "failed to get probation list", err)
   151  	}
   152  	if epochData.Height >= config.ReadConfig.Nsv2height {
   153  		return delegatesV2(probationList, response, &message)
   154  	}
   155  	for rank, bp := range response.BlockProducersInfo {
   156  		votes, ok := new(big.Int).SetString(bp.Votes, 10)
   157  		if !ok {
   158  			return output.NewError(output.ConvertError, "failed to convert votes into big int", nil)
   159  		}
   160  		isProbated := false
   161  		if _, ok := probationList.ProbationInfo[bp.Address]; ok {
   162  			// if it exists in probation info
   163  			isProbated = true
   164  		}
   165  		delegate := delegate{
   166  			Address:        bp.Address,
   167  			Rank:           rank + 1,
   168  			Alias:          aliases[bp.Address],
   169  			Active:         bp.Active,
   170  			Production:     int(bp.Production),
   171  			Votes:          util.RauToString(votes, util.IotxDecimalNum),
   172  			ProbatedStatus: isProbated,
   173  		}
   174  		message.Delegates = append(message.Delegates, delegate)
   175  	}
   176  	return sortAndPrint(&message)
   177  }
   178  
   179  func delegatesV2(pb *vote.ProbationList, epochMeta *iotexapi.GetEpochMetaResponse, message *delegatesMessage) error {
   180  	conn, err := util.ConnectToEndpoint(config.ReadConfig.SecureConnect && !config.Insecure)
   181  	if err != nil {
   182  		return output.NewError(output.NetworkError, "failed to connect to endpoint", err)
   183  	}
   184  	defer conn.Close()
   185  
   186  	cli := iotexapi.NewAPIServiceClient(conn)
   187  	ctx := context.Background()
   188  
   189  	jwtMD, err := util.JwtAuth()
   190  	if err == nil {
   191  		ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx)
   192  	}
   193  
   194  	request := &iotexapi.ReadStateRequest{
   195  		ProtocolID: []byte("poll"),
   196  		MethodName: []byte("ActiveBlockProducersByEpoch"),
   197  		Arguments:  [][]byte{[]byte(strconv.FormatUint(epochMeta.EpochData.Num, 10))},
   198  		Height:     strconv.FormatUint(epochMeta.EpochData.Height, 10),
   199  	}
   200  	abpResponse, err := cli.ReadState(ctx, request)
   201  	if err != nil {
   202  		sta, ok := status.FromError(err)
   203  		if ok && sta.Code() == codes.NotFound {
   204  			fmt.Println(message.String())
   205  			return nil
   206  		} else if ok {
   207  			return output.NewError(output.APIError, sta.Message(), nil)
   208  		}
   209  		return output.NewError(output.NetworkError, "failed to invoke ReadState api", err)
   210  	}
   211  	var ABPs state.CandidateList
   212  	if err := ABPs.Deserialize(abpResponse.Data); err != nil {
   213  		return output.NewError(output.SerializationError, "failed to deserialize active BPs", err)
   214  	}
   215  	request = &iotexapi.ReadStateRequest{
   216  		ProtocolID: []byte("poll"),
   217  		MethodName: []byte("BlockProducersByEpoch"),
   218  		Arguments:  [][]byte{[]byte(strconv.FormatUint(epochMeta.EpochData.Num, 10))},
   219  	}
   220  	bpResponse, err := cli.ReadState(ctx, request)
   221  	if err != nil {
   222  		sta, ok := status.FromError(err)
   223  		if ok {
   224  			return output.NewError(output.APIError, sta.Message(), nil)
   225  		}
   226  		return output.NewError(output.NetworkError, "failed to invoke ReadState api", err)
   227  	}
   228  	var BPs state.CandidateList
   229  	if err := BPs.Deserialize(bpResponse.Data); err != nil {
   230  		return output.NewError(output.SerializationError, "failed to deserialize BPs", err)
   231  	}
   232  	isActive := make(map[string]bool)
   233  	for _, abp := range ABPs {
   234  		isActive[abp.Address] = true
   235  	}
   236  	production := make(map[string]int)
   237  	for _, info := range epochMeta.BlockProducersInfo {
   238  		production[info.Address] = int(info.Production)
   239  	}
   240  	aliases := alias.GetAliasMap()
   241  	for rank, bp := range BPs {
   242  		isProbated := false
   243  		if _, ok := pb.ProbationInfo[bp.Address]; ok {
   244  			isProbated = true
   245  		}
   246  		votes := big.NewInt(0).SetBytes(bp.Votes.Bytes())
   247  		message.Delegates = append(message.Delegates, delegate{
   248  			Address:        bp.Address,
   249  			Rank:           rank + 1,
   250  			Alias:          aliases[bp.Address],
   251  			Active:         isActive[bp.Address],
   252  			Production:     production[bp.Address],
   253  			Votes:          util.RauToString(votes, util.IotxDecimalNum),
   254  			ProbatedStatus: isProbated,
   255  		})
   256  	}
   257  	if err = fillMessage(cli, message, aliases, isActive, pb); err != nil {
   258  		return err
   259  	}
   260  	return sortAndPrint(message)
   261  }
   262  
   263  func sortAndPrint(message *delegatesMessage) error {
   264  	if _allFlag.Value() == false && len(message.Delegates) > _defaultDelegateNum {
   265  		message.Delegates = message.Delegates[:_defaultDelegateNum]
   266  		fmt.Println(message.String())
   267  		return nil
   268  	}
   269  	for i := _defaultDelegateNum; i < len(message.Delegates); i++ {
   270  		totalWeightedVotes, ok := big.NewFloat(0).SetString(message.Delegates[i].Votes)
   271  		if !ok {
   272  			return errors.New("string convert to big float")
   273  		}
   274  		totalWeightedVotesInt, _ := totalWeightedVotes.Int(nil)
   275  		message.Delegates[i].TotalWeightedVotes = totalWeightedVotesInt
   276  	}
   277  	if len(message.Delegates) > _defaultDelegateNum {
   278  		latter := message.Delegates[_defaultDelegateNum:]
   279  		message.Delegates = message.Delegates[:_defaultDelegateNum]
   280  		sort.SliceStable(latter, func(i, j int) bool {
   281  			return latter[i].TotalWeightedVotes.Cmp(latter[j].TotalWeightedVotes) > 0
   282  		})
   283  		for i, t := range latter {
   284  			t.Rank = _defaultDelegateNum + i + 1
   285  			message.Delegates = append(message.Delegates, t)
   286  		}
   287  	}
   288  	fmt.Println(message.String())
   289  	return nil
   290  }
   291  
   292  func getProbationList(_epochNum uint64, epochStartHeight uint64) (*vote.ProbationList, error) {
   293  	probationListRes, err := bc.GetProbationList(_epochNum, epochStartHeight)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	probationList := &vote.ProbationList{}
   298  	if probationListRes != nil {
   299  		if err := probationList.Deserialize(probationListRes.Data); err != nil {
   300  			return nil, err
   301  		}
   302  	}
   303  	return probationList, nil
   304  }
   305  
   306  func fillMessage(cli iotexapi.APIServiceClient, message *delegatesMessage, alias map[string]string, active map[string]bool, pb *vote.ProbationList) error {
   307  	cl, err := getAllStakingCandidates(cli)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	addressMap := make(map[string]*iotextypes.CandidateV2)
   312  	for _, candidate := range cl.Candidates {
   313  		addressMap[candidate.OperatorAddress] = candidate
   314  	}
   315  	delegateAddressMap := make(map[string]struct{})
   316  	for _, m := range message.Delegates {
   317  		delegateAddressMap[m.Address] = struct{}{}
   318  	}
   319  	for i, m := range message.Delegates {
   320  		if c, ok := addressMap[m.Address]; ok {
   321  			message.Delegates[i].Name = c.Name
   322  			continue
   323  		}
   324  	}
   325  	rank := len(message.Delegates) + 1
   326  	for _, candidate := range cl.Candidates {
   327  		if _, ok := delegateAddressMap[candidate.OperatorAddress]; ok {
   328  			continue
   329  		}
   330  		isProbated := false
   331  		if _, ok := pb.ProbationInfo[candidate.OperatorAddress]; ok {
   332  			isProbated = true
   333  		}
   334  		iotx, err := util.StringToIOTX(candidate.TotalWeightedVotes)
   335  		if err != nil {
   336  			return err
   337  		}
   338  		message.Delegates = append(message.Delegates, delegate{
   339  			Address:        candidate.OperatorAddress,
   340  			Name:           candidate.Name,
   341  			Rank:           rank,
   342  			Alias:          alias[candidate.OperatorAddress],
   343  			Active:         active[candidate.OperatorAddress],
   344  			Votes:          iotx,
   345  			ProbatedStatus: isProbated,
   346  		})
   347  		rank++
   348  	}
   349  	return nil
   350  }
   351  
   352  func getAllStakingCandidates(chainClient iotexapi.APIServiceClient) (candidateListAll *iotextypes.CandidateListV2, err error) {
   353  	candidateListAll = &iotextypes.CandidateListV2{}
   354  	for i := uint32(0); ; i++ {
   355  		offset := i * _readCandidatesLimit
   356  		size := uint32(_readCandidatesLimit)
   357  		candidateList, err := util.GetStakingCandidates(chainClient, offset, size)
   358  		if err != nil {
   359  			return nil, errors.Wrap(err, "failed to get candidates")
   360  		}
   361  		candidateListAll.Candidates = append(candidateListAll.Candidates, candidateList.Candidates...)
   362  		if len(candidateList.Candidates) < _readCandidatesLimit {
   363  			break
   364  		}
   365  	}
   366  	return
   367  }
   368  
   369  func getCandidateRewardAddressByAddressOrName(cli iotexapi.APIServiceClient, name string) (string, error) {
   370  	address, err1 := util.Address(name)
   371  	if err1 == nil {
   372  		return address, nil
   373  	}
   374  	cl, err := getAllStakingCandidates(cli)
   375  	if err != nil {
   376  		return "", err
   377  	}
   378  	for _, candidate := range cl.Candidates {
   379  		if candidate.Name == name {
   380  			return candidate.RewardAddress, nil
   381  		}
   382  	}
   383  	return "", err1
   384  }