-
Notifications
You must be signed in to change notification settings - Fork 55
/
validatePrimary.go
381 lines (354 loc) · 12.7 KB
/
validatePrimary.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Copyright (C) 2022, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package nodecmd
import (
"errors"
"fmt"
"os"
"strconv"
"time"
subnetcmd "github.com/ava-labs/avalanche-cli/cmd/subnetcmd"
"github.com/ava-labs/avalanche-cli/pkg/ansible"
"github.com/ava-labs/avalanche-cli/pkg/cobrautils"
"github.com/ava-labs/avalanche-cli/pkg/constants"
"github.com/ava-labs/avalanche-cli/pkg/keychain"
"github.com/ava-labs/avalanche-cli/pkg/models"
"github.com/ava-labs/avalanche-cli/pkg/subnet"
"github.com/ava-labs/avalanche-cli/pkg/utils"
"github.com/ava-labs/avalanche-cli/pkg/ux"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/units"
"github.com/ava-labs/avalanchego/vms/platformvm"
"github.com/ava-labs/avalanchego/vms/platformvm/signer"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
)
var (
keyName string
useEwoq bool
useLedger bool
useStaticIP bool
awsProfile string
ledgerAddresses []string
weight uint64
startTimeStr string
duration time.Duration
defaultValidatorParams bool
useCustomDuration bool
ErrMutuallyExlusiveKeyLedger = errors.New("--key and --ledger,--ledger-addrs are mutually exclusive")
ErrStoredKeyOnMainnet = errors.New("--key is not available for mainnet operations")
ErrNoBlockchainID = errors.New("failed to find the blockchain ID for this subnet, has it been deployed/created on this network?")
ErrNoSubnetID = errors.New("failed to find the subnet ID for this subnet, has it been deployed/created on this network?")
)
func newValidatePrimaryCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "primary [clusterName]",
Short: "(ALPHA Warning) Join Primary Network as a validator",
Long: `(ALPHA Warning) This command is currently in experimental mode.
The node validate primary command enables all nodes in a cluster to be validators of Primary
Network.`,
Args: cobrautils.ExactArgs(1),
RunE: validatePrimaryNetwork,
}
cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji only]")
cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on fuji/devnet)")
cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [fuji/devnet only]")
cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses")
cmd.Flags().Uint64Var(&weight, "stake-amount", 0, "how many AVAX to stake in the validator")
cmd.Flags().StringVar(&startTimeStr, "start-time", "", "UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format")
cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long validator validates for after start time")
return cmd
}
func GetMinStakingAmount(network models.Network) (uint64, error) {
pClient := platformvm.NewClient(network.Endpoint)
ctx, cancel := utils.GetAPIContext()
defer cancel()
minValStake, _, err := pClient.GetMinStake(ctx, ids.Empty)
if err != nil {
return 0, err
}
return minValStake, nil
}
func joinAsPrimaryNetworkValidator(
deployer *subnet.PublicDeployer,
network models.Network,
kc *keychain.Keychain,
nodeID ids.NodeID,
nodeIndex int,
signingKeyPath string,
nodeCmd bool,
) error {
ux.Logger.PrintToUser(fmt.Sprintf("Adding node %s as a Primary Network Validator...", nodeID.String()))
defer ux.Logger.PrintLineSeparator()
var (
start time.Time
err error
)
minValStake, err := GetMinStakingAmount(network)
if err != nil {
return err
}
if weight == 0 {
weight, err = PromptWeightPrimaryNetwork(network)
if err != nil {
return err
}
}
if weight < minValStake {
return fmt.Errorf("illegal weight, must be greater than or equal to %d: %d", minValStake, weight)
}
start, duration, err = GetTimeParametersPrimaryNetwork(network, nodeIndex, duration, startTimeStr, nodeCmd)
if err != nil {
return err
}
recipientAddr := kc.Addresses().List()[0]
PrintNodeJoinPrimaryNetworkOutput(nodeID, weight, network, start)
// we set the starting time for node to be a Primary Network Validator to be in 1 minute
// we use min delegation fee as default
delegationFee := network.GenesisParams().MinDelegationFee
blsKeyBytes, err := os.ReadFile(signingKeyPath)
if err != nil {
return err
}
blsSk, err := bls.SecretKeyFromBytes(blsKeyBytes)
if err != nil {
return err
}
if _, err := deployer.AddPermissionlessValidator(
ids.Empty,
ids.Empty,
nodeID,
weight,
uint64(start.Unix()),
uint64(start.Add(duration).Unix()),
recipientAddr,
delegationFee,
nil,
signer.NewProofOfPossession(blsSk),
); err != nil {
return err
}
ux.Logger.PrintToUser(fmt.Sprintf("Node %s successfully added as Primary Network validator!", nodeID.String()))
return nil
}
func PromptWeightPrimaryNetwork(network models.Network) (uint64, error) {
defaultStake := network.GenesisParams().MinValidatorStake
defaultWeight := fmt.Sprintf("Default (%s)", convertNanoAvaxToAvaxString(defaultStake))
txt := "What stake weight would you like to assign to the validator?"
weightOptions := []string{defaultWeight, "Custom"}
weightOption, err := app.Prompt.CaptureList(txt, weightOptions)
if err != nil {
return 0, err
}
switch weightOption {
case defaultWeight:
return defaultStake, nil
default:
return app.Prompt.CaptureWeight(txt)
}
}
func GetTimeParametersPrimaryNetwork(network models.Network, nodeIndex int, validationDuration time.Duration, validationStartTimeStr string, nodeCmd bool) (time.Time, time.Duration, error) {
const (
defaultDurationOption = "Minimum staking duration on primary network"
custom = "Custom"
)
var err error
var start time.Time
if validationStartTimeStr != "" {
start, err = time.Parse(constants.TimeParseLayout, validationStartTimeStr)
if err != nil {
return time.Time{}, 0, err
}
} else {
start = time.Now().Add(constants.PrimaryNetworkValidatingStartLeadTimeNodeCmd)
if !nodeCmd {
start = time.Now().Add(constants.PrimaryNetworkValidatingStartLeadTime)
}
}
if useCustomDuration && validationDuration != 0 {
return start, duration, nil
}
if validationDuration != 0 {
duration, err = getDefaultValidationTime(start, network, nodeIndex)
if err != nil {
return time.Time{}, 0, err
}
return start, duration, nil
}
msg := "How long should your validator validate for?"
durationOptions := []string{defaultDurationOption, custom}
durationOption, err := app.Prompt.CaptureList(msg, durationOptions)
if err != nil {
return time.Time{}, 0, err
}
switch durationOption {
case defaultDurationOption:
duration, err = getDefaultValidationTime(start, network, nodeIndex)
if err != nil {
return time.Time{}, 0, err
}
default:
useCustomDuration = true
duration, err = subnetcmd.PromptDuration(start, network)
if err != nil {
return time.Time{}, 0, err
}
}
return start, duration, nil
}
func getDefaultValidationTime(start time.Time, network models.Network, nodeIndex int) (time.Duration, error) {
durationStr := constants.DefaultFujiStakeDuration
if network.Kind == models.Mainnet {
durationStr = constants.DefaultMainnetStakeDuration
}
durationInt, err := strconv.Atoi(durationStr[:len(durationStr)-1])
if err != nil {
return 0, err
}
// stagger expiration time by 1 day for each added node
durationAddition := 24 * nodeIndex
durationStr = strconv.Itoa(durationInt+durationAddition) + "h"
d, err := time.ParseDuration(durationStr)
if err != nil {
return 0, err
}
end := start.Add(d)
if nodeIndex == 0 {
confirm := fmt.Sprintf("Your validator will finish staking by %s", end.Format(constants.TimeParseLayout))
yes, err := app.Prompt.CaptureYesNo(confirm)
if err != nil {
return 0, err
}
if !yes {
return 0, errors.New("you have to confirm staking duration")
}
}
return d, nil
}
func getNodeIDs(hosts []*models.Host) (map[string]ids.NodeID, map[string]error) {
nodeIDMap := map[string]ids.NodeID{}
failedNodes := map[string]error{}
for _, host := range hosts {
cloudNodeID := host.GetCloudID()
nodeID, err := getNodeID(app.GetNodeInstanceDirPath(cloudNodeID))
if err != nil {
failedNodes[host.NodeID] = err
continue
}
nodeIDMap[host.NodeID] = nodeID
}
return nodeIDMap, failedNodes
}
// checkNodeIsPrimaryNetworkValidator returns true if node is already a Primary Network validator
func checkNodeIsPrimaryNetworkValidator(nodeID ids.NodeID, network models.Network) (bool, error) {
isValidator, err := subnet.IsSubnetValidator(ids.Empty, nodeID, network)
if err != nil {
return false, err
}
return isValidator, nil
}
// addNodeAsPrimaryNetworkValidator returns bool if node is added as primary network validator
// as it impacts the output in adding node as subnet validator in the next steps
func addNodeAsPrimaryNetworkValidator(
deployer *subnet.PublicDeployer,
network models.Network,
kc *keychain.Keychain,
nodeID ids.NodeID,
nodeIndex int,
instanceID string,
) error {
if isValidator, err := checkNodeIsPrimaryNetworkValidator(nodeID, network); err != nil {
return err
} else if !isValidator {
signingKeyPath := app.GetNodeBLSSecretKeyPath(instanceID)
return joinAsPrimaryNetworkValidator(deployer, network, kc, nodeID, nodeIndex, signingKeyPath, true)
}
return nil
}
func validatePrimaryNetwork(_ *cobra.Command, args []string) error {
clusterName := args[0]
if err := checkCluster(clusterName); err != nil {
return err
}
clusterConfig, err := app.GetClusterConfig(clusterName)
if err != nil {
return err
}
network := clusterConfig.Network
allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName))
if err != nil {
return err
}
hosts := clusterConfig.GetValidatorHosts(allHosts) // exlude api nodes
defer disconnectHosts(hosts)
fee := network.GenesisParams().AddPrimaryNetworkValidatorFee * uint64(len(hosts))
kc, err := keychain.GetKeychainFromCmdLineFlags(
app,
constants.PayTxsFeesMsg,
network,
keyName,
useEwoq,
useLedger,
ledgerAddresses,
fee,
)
if err != nil {
return err
}
deployer := subnet.NewPublicDeployer(app, kc, network)
if err := checkHostsAreBootstrapped(hosts); err != nil {
return err
}
if err := checkHostsAreHealthy(hosts); err != nil {
return err
}
ux.Logger.PrintToUser("Note that we have staggered the end time of validation period to increase by 24 hours for each node added if multiple nodes are added as Primary Network validators simultaneously")
nodeIDMap, failedNodesMap := getNodeIDs(hosts)
nodeErrors := map[string]error{}
for i, host := range hosts {
nodeID, b := nodeIDMap[host.NodeID]
if !b {
err, b := failedNodesMap[host.NodeID]
if !b {
return fmt.Errorf("expected to found an error for non mapped node")
}
ux.Logger.PrintToUser("Failed to add node %s as Primary Network validator due to %s", host.NodeID, err)
nodeErrors[host.NodeID] = err
continue
}
_, clusterNodeID, err := models.HostAnsibleIDToCloudID(host.NodeID)
if err != nil {
ux.Logger.PrintToUser("Failed to add node %s as Primary Network due to %s", host.NodeID, err.Error())
nodeErrors[host.NodeID] = err
continue
}
if err = addNodeAsPrimaryNetworkValidator(deployer, network, kc, nodeID, i, clusterNodeID); err != nil {
ux.Logger.PrintToUser("Failed to add node %s as Primary Network validator due to %s", host.NodeID, err)
nodeErrors[host.NodeID] = err
}
}
if len(nodeErrors) > 0 {
ux.Logger.PrintToUser("Failed nodes: ")
for node, nodeErr := range nodeErrors {
ux.Logger.PrintToUser("node %s failed due to %v", node, nodeErr)
}
return fmt.Errorf("node(s) %s failed to validate the Primary Network", maps.Keys(nodeErrors))
} else {
ux.Logger.PrintToUser(fmt.Sprintf("All nodes in cluster %s are successfully added as Primary Network validators!", clusterName))
}
return nil
}
// convertNanoAvaxToAvaxString converts nanoAVAX to AVAX
func convertNanoAvaxToAvaxString(weight uint64) string {
return fmt.Sprintf("%.2f %s", float64(weight)/float64(units.Avax), constants.AVAXSymbol)
}
func PrintNodeJoinPrimaryNetworkOutput(nodeID ids.NodeID, weight uint64, network models.Network, start time.Time) {
ux.Logger.PrintToUser("NodeID: %s", nodeID.String())
ux.Logger.PrintToUser("Network: %s", network.Name())
ux.Logger.PrintToUser("Start time: %s", start.Format(constants.TimeParseLayout))
ux.Logger.PrintToUser("End time: %s", start.Add(duration).Format(constants.TimeParseLayout))
// we need to divide by 10 ^ 9 since we were using nanoAvax
ux.Logger.PrintToUser("Weight: %s", convertNanoAvaxToAvaxString(weight))
ux.Logger.PrintToUser("Inputs complete, issuing transaction to add the provided validator information...")
}