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 }