-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathadd_validator.go
More file actions
858 lines (780 loc) · 27.4 KB
/
add_validator.go
File metadata and controls
858 lines (780 loc) · 27.4 KB
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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
// Copyright (C) 2022, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockchaincmd
import (
"errors"
"fmt"
"math/big"
"strings"
"time"
"github.com/ava-labs/avalanche-cli/pkg/blockchain"
"github.com/ava-labs/avalanchego/config"
"github.com/ava-labs/avalanchego/utils/units"
"github.com/ethereum/go-ethereum/common"
"github.com/ava-labs/avalanche-cli/pkg/cobrautils"
"github.com/ava-labs/avalanche-cli/pkg/constants"
"github.com/ava-labs/avalanche-cli/pkg/contract"
"github.com/ava-labs/avalanche-cli/pkg/keychain"
"github.com/ava-labs/avalanche-cli/pkg/models"
"github.com/ava-labs/avalanche-cli/pkg/networkoptions"
"github.com/ava-labs/avalanche-cli/pkg/node"
"github.com/ava-labs/avalanche-cli/pkg/prompts"
"github.com/ava-labs/avalanche-cli/pkg/subnet"
"github.com/ava-labs/avalanche-cli/pkg/txutils"
"github.com/ava-labs/avalanche-cli/pkg/utils"
"github.com/ava-labs/avalanche-cli/pkg/ux"
"github.com/ava-labs/avalanche-cli/pkg/validatormanager"
"github.com/ava-labs/avalanche-cli/sdk/validator"
"github.com/ava-labs/avalanchego/ids"
avagoconstants "github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/formatting/address"
"github.com/ava-labs/avalanchego/utils/logging"
warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message"
"github.com/spf13/cobra"
)
var (
nodeIDStr string
nodeEndpoint string
balanceAVAX float64
weight uint64
startTimeStr string
duration time.Duration
defaultValidatorParams bool
useDefaultStartTime bool
useDefaultDuration bool
useDefaultWeight bool
waitForTxAcceptance bool
publicKey string
pop string
remainingBalanceOwnerAddr string
disableOwnerAddr string
rpcURL string
aggregatorLogLevel string
aggregatorLogToStdout bool
delegationFee uint16
errNoSubnetID = errors.New("failed to find the subnet ID for this subnet, has it been deployed/created on this network?")
errMutuallyExclusiveDurationOptions = errors.New("--use-default-duration/--use-default-validator-params and --staking-period are mutually exclusive")
errMutuallyExclusiveStartOptions = errors.New("--use-default-start-time/--use-default-validator-params and --start-time are mutually exclusive")
errMutuallyExclusiveWeightOptions = errors.New("--use-default-validator-params and --weight are mutually exclusive")
ErrNotPermissionedSubnet = errors.New("subnet is not permissioned")
aggregatorExtraEndpoints []string
aggregatorAllowPrivatePeers bool
clusterNameFlagValue string
createLocalValidator bool
)
// avalanche blockchain addValidator
func newAddValidatorCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "addValidator [blockchainName]",
Short: "Add a validator to an L1",
Long: `The blockchain addValidator command adds a node as a validator to
an L1 of the user provided deployed network. If the network is proof of
authority, the owner of the validator manager contract must sign the
transaction. If the network is proof of stake, the node must stake the L1's
staking token. Both processes will issue a RegisterL1ValidatorTx on the P-Chain.
This command currently only works on Blockchains deployed to either the Fuji
Testnet or Mainnet.`,
RunE: addValidator,
Args: cobrautils.ExactArgs(1),
}
networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, true, networkoptions.DefaultSupportedNetworkOptions)
cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji/devnet only]")
cmd.Flags().Uint64Var(&weight, "weight", constants.NonBootstrapValidatorWeight, "set the staking weight of the validator to add")
cmd.Flags().Float64Var(
&balanceAVAX,
"balance",
0,
"set the AVAX balance of the validator that will be used for continuous fee on P-Chain",
)
cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [fuji/devnet 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().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses")
cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "node-id of the validator to add")
cmd.Flags().StringVar(&publicKey, "bls-public-key", "", "set the BLS public key of the validator to add")
cmd.Flags().StringVar(&pop, "bls-proof-of-possession", "", "set the BLS proof of possession of the validator to add")
cmd.Flags().StringVar(&remainingBalanceOwnerAddr, "remaining-balance-owner", "", "P-Chain address that will receive any leftover AVAX from the validator when it is removed from Subnet")
cmd.Flags().StringVar(&disableOwnerAddr, "disable-owner", "", "P-Chain address that will able to disable the validator with a P-Chain transaction")
cmd.Flags().BoolVar(&createLocalValidator, "create-local-validator", false, "create additional local validator and add it to existing running local node")
cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "set primary network partial sync for new validators")
cmd.Flags().StringVar(&nodeEndpoint, "node-endpoint", "", "gather node id/bls from publicly available avalanchego apis on the given endpoint")
cmd.Flags().StringSliceVar(&aggregatorExtraEndpoints, "aggregator-extra-endpoints", nil, "endpoints for extra nodes that are needed in signature aggregation")
cmd.Flags().BoolVar(&aggregatorAllowPrivatePeers, "aggregator-allow-private-peers", true, "allow the signature aggregator to connect to peers with private IP")
privateKeyFlags.AddToCmd(cmd, "to pay fees for completing the validator's registration (blockchain gas token)")
cmd.Flags().StringVar(&rpcURL, "rpc", "", "connect to validator manager at the given rpc endpoint")
cmd.Flags().StringVar(&aggregatorLogLevel, "aggregator-log-level", constants.DefaultAggregatorLogLevel, "log level to use with signature aggregator")
cmd.Flags().BoolVar(&aggregatorLogToStdout, "aggregator-log-to-stdout", false, "use stdout for signature aggregator logs")
cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long this validator will be staking")
cmd.Flags().BoolVar(&useDefaultStartTime, "default-start-time", false, "(for Subnets, not L1s) use default start time for subnet validator (5 minutes later for fuji & mainnet, 30 seconds later for devnet)")
cmd.Flags().StringVar(&startTimeStr, "start-time", "", "(for Subnets, not L1s) UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format")
cmd.Flags().BoolVar(&useDefaultDuration, "default-duration", false, "(for Subnets, not L1s) set duration so as to validate until primary validator ends its period")
cmd.Flags().BoolVar(&defaultValidatorParams, "default-validator-params", false, "(for Subnets, not L1s) use default weight/start/duration params for subnet validator")
cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "(for Subnets, not L1s) control keys that will be used to authenticate add validator tx")
cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "(for Subnets, not L1s) file path of the add validator tx")
cmd.Flags().BoolVar(&waitForTxAcceptance, "wait-for-tx-acceptance", true, "(for Subnets, not L1s) just issue the add validator tx, without waiting for its acceptance")
cmd.Flags().Uint64Var(&stakeAmount, "stake-amount", 0, "(PoS only) amount of tokens to stake")
cmd.Flags().Uint16Var(&delegationFee, "delegation-fee", 100, "(PoS only) delegation fee (in bips)")
return cmd
}
func preAddChecks() error {
if nodeEndpoint != "" && createLocalValidator {
return fmt.Errorf("cannot set both --node-endpoint and --create-local-validator")
}
if createLocalValidator && (nodeIDStr != "" || publicKey != "" || pop != "") {
return fmt.Errorf("cannot set --node-id, --bls-public-key or --bls-proof-of-possession if --create-local-validator used")
}
return nil
}
func addValidator(_ *cobra.Command, args []string) error {
blockchainName := args[0]
_, err := ValidateSubnetNameAndGetChains([]string{blockchainName})
if err != nil {
return err
}
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return fmt.Errorf("failed to load sidecar: %w", err)
}
network, err := networkoptions.GetNetworkFromCmdLineFlags(
app,
"",
globalNetworkFlags,
true,
false,
networkoptions.GetNetworkFromSidecar(sc, networkoptions.DefaultSupportedNetworkOptions),
"",
)
if err != nil {
return err
}
if network.ClusterName != "" {
clusterNameFlagValue = network.ClusterName
network = models.ConvertClusterToNetwork(network)
}
if err := preAddChecks(); err != nil {
return err
}
if sc.Networks[network.Name()].ClusterName != "" {
clusterNameFlagValue = sc.Networks[network.Name()].ClusterName
}
fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.AddSubnetValidatorFee
kc, err := keychain.GetKeychainFromCmdLineFlags(
app,
"to pay for transaction fees on P-Chain",
network,
keyName,
useEwoq,
useLedger,
ledgerAddresses,
fee,
)
if err != nil {
return err
}
sovereign := sc.Sovereign
if nodeEndpoint != "" {
nodeIDStr, publicKey, pop, err = node.GetNodeData(nodeEndpoint)
if err != nil {
return err
}
}
// if we don't have a nodeID or ProofOfPossession by this point, prompt user if we want to add a aditional local node
if (!sovereign && nodeIDStr == "") || (sovereign && !createLocalValidator && nodeIDStr == "" && publicKey == "" && pop == "") {
for {
local := "Use my local machine to spin up an additional validator"
existing := "I have an existing Avalanche node (we will require its NodeID and BLS info)"
if option, err := app.Prompt.CaptureList(
"How would you like to set up the new validator",
[]string{local, existing},
); err != nil {
return err
} else {
createLocalValidator = option == local
break
}
}
}
subnetID := sc.Networks[network.Name()].SubnetID
// if user chose to upsize a local node to add another local validator
if createLocalValidator {
anrSettings := node.ANRSettings{}
nodeConfig := map[string]interface{}{}
ux.Logger.PrintToUser("Creating a new Avalanche node on local machine to add as a new validator to blockchain %s", blockchainName)
if app.AvagoNodeConfigExists(blockchainName) {
nodeConfig, err = utils.ReadJSON(app.GetAvagoNodeConfigPath(blockchainName))
if err != nil {
return err
}
}
if partialSync {
nodeConfig[config.PartialSyncPrimaryNetworkKey] = true
}
avalancheGoBinPath, err := node.GetLocalNodeAvalancheGoBinPath()
if err != nil {
return fmt.Errorf("failed to get local node avalanche go bin path: %w", err)
}
nodeName := ""
blockchainID := sc.Networks[network.Name()].BlockchainID
if nodeName, err = node.UpsizeLocalNode(
app,
network,
blockchainName,
blockchainID,
subnetID,
avalancheGoBinPath,
nodeConfig,
anrSettings,
); err != nil {
return err
}
// get node data
nodeInfo, err := node.GetNodeInfo(nodeName)
if err != nil {
return err
}
nodeIDStr, publicKey, pop, err = node.GetNodeData(nodeInfo.Uri)
if err != nil {
return err
}
// update sidecar with new node
if err := node.AddNodeInfoToSidecar(&sc, nodeInfo, network); err != nil {
return err
}
if err := app.UpdateSidecar(&sc); err != nil {
return err
}
// make sure extra validator endpoint added for the new node
aggregatorExtraEndpoints = append(aggregatorExtraEndpoints, constants.LocalAPIEndpoint)
}
if nodeIDStr == "" {
nodeID, err := PromptNodeID("add as a blockchain validator")
if err != nil {
return err
}
nodeIDStr = nodeID.String()
}
if err := prompts.ValidateNodeID(nodeIDStr); err != nil {
return err
}
if sovereign && publicKey == "" && pop == "" {
publicKey, pop, err = promptProofOfPossession(true, true)
if err != nil {
return err
}
}
network.HandlePublicNetworkSimulation()
if !sovereign {
if err := UpdateKeychainWithSubnetControlKeys(kc, network, blockchainName); err != nil {
return err
}
}
deployer := subnet.NewPublicDeployer(app, kc, network)
if !sovereign {
return CallAddValidatorNonSOV(deployer, network, kc, useLedger, blockchainName, nodeIDStr, defaultValidatorParams, waitForTxAcceptance)
}
return CallAddValidator(
deployer,
network,
kc,
blockchainName,
subnetID,
nodeIDStr,
publicKey,
pop,
weight,
balanceAVAX,
remainingBalanceOwnerAddr,
disableOwnerAddr,
)
}
func promptValidatorBalanceAVAX(availableBalance float64) (float64, error) {
ux.Logger.PrintToUser("Validator's balance is used to pay for continuous fee to the P-Chain")
ux.Logger.PrintToUser("When this Balance reaches 0, the validator will be considered inactive and will no longer participate in validating the L1")
txt := "What balance would you like to assign to the validator (in AVAX)?"
return app.Prompt.CaptureValidatorBalance(txt, availableBalance, constants.BootstrapValidatorBalanceAVAX)
}
func CallAddValidator(
deployer *subnet.PublicDeployer,
network models.Network,
kc *keychain.Keychain,
blockchainName string,
subnetID ids.ID,
nodeIDStr string,
publicKey string,
pop string,
weight uint64,
balanceAVAX float64,
remainingBalanceOwnerAddr string,
disableOwnerAddr string,
) error {
nodeID, err := ids.NodeIDFromString(nodeIDStr)
if err != nil {
return err
}
blsInfo, err := blockchain.ConvertToBLSProofOfPossession(publicKey, pop)
if err != nil {
return fmt.Errorf("failure parsing BLS info: %w", err)
}
blockchainTimestamp, err := blockchain.GetBlockchainTimestamp(network)
if err != nil {
return fmt.Errorf("failed to get blockchain timestamp: %w", err)
}
expiry := uint64(blockchainTimestamp.Add(constants.DefaultValidationIDExpiryDuration).Unix())
chainSpec := contract.ChainSpec{
BlockchainName: blockchainName,
}
sc, err := app.LoadSidecar(chainSpec.BlockchainName)
if err != nil {
return fmt.Errorf("failed to load sidecar: %w", err)
}
if sc.Networks[network.Name()].ValidatorManagerAddress == "" {
return fmt.Errorf("unable to find Validator Manager address")
}
validatorManagerAddress = sc.Networks[network.Name()].ValidatorManagerAddress
ownerPrivateKeyFound, _, _, ownerPrivateKey, err := contract.SearchForManagedKey(
app,
network,
common.HexToAddress(sc.ValidatorManagerOwner),
true,
)
if err != nil {
return err
}
if !ownerPrivateKeyFound {
return fmt.Errorf("private key for Validator manager owner %s is not found", sc.ValidatorManagerOwner)
}
pos := sc.PoS()
if pos {
// should take input prior to here for stake amount, delegation fee, and min stake duration
if stakeAmount == 0 {
stakeAmount, err = app.Prompt.CaptureUint64Compare(
fmt.Sprintf("Enter the amount of %s to stake ", sc.TokenName),
[]prompts.Comparator{
{
Label: "Positive",
Type: prompts.MoreThan,
Value: 0,
},
},
)
if err != nil {
return err
}
}
if duration == 0 {
duration, err = PromptDuration(time.Now(), network, true) // it's pos
if err != nil {
return nil
}
}
}
ux.Logger.PrintToUser(logging.Yellow.Wrap("Validation manager owner %s pays for the initialization of the validator's registration (Blockchain gas token)"), sc.ValidatorManagerOwner)
if rpcURL == "" {
rpcURL, _, err = contract.GetBlockchainEndpoints(
app,
network,
chainSpec,
true,
false,
)
if err != nil {
return err
}
}
ux.Logger.PrintToUser(logging.Yellow.Wrap("RPC Endpoint: %s"), rpcURL)
totalWeight, err := validator.GetTotalWeight(network.SDKNetwork(), subnetID)
if err != nil {
return err
}
allowedChange := float64(totalWeight) * constants.MaxL1TotalWeightChange
if float64(weight) > allowedChange {
return fmt.Errorf("can't make change: desired validator weight %d exceeds max allowed weight change of %d", newWeight, uint64(allowedChange))
}
if balanceAVAX == 0 {
availableBalance, err := utils.GetNetworkBalance(kc.Addresses().List(), network.Endpoint)
if err != nil {
return err
}
balanceAVAX, err = promptValidatorBalanceAVAX(float64(availableBalance) / float64(units.Avax))
if err != nil {
return err
}
}
// convert to nanoAVAX
balance := uint64(balanceAVAX * float64(units.Avax))
if remainingBalanceOwnerAddr == "" {
remainingBalanceOwnerAddr, err = blockchain.GetKeyForChangeOwner(app, network)
if err != nil {
return err
}
}
remainingBalanceOwnerAddrID, err := address.ParseToIDs([]string{remainingBalanceOwnerAddr})
if err != nil {
return fmt.Errorf("failure parsing remaining balanche owner address %s: %w", remainingBalanceOwnerAddr, err)
}
remainingBalanceOwners := warpMessage.PChainOwner{
Threshold: 1,
Addresses: remainingBalanceOwnerAddrID,
}
if disableOwnerAddr == "" {
disableOwnerAddr, err = prompts.PromptAddress(
app.Prompt,
"be able to disable the validator using P-Chain transactions",
app.GetKeyDir(),
app.GetKey,
"",
network,
prompts.PChainFormat,
"Enter P-Chain address (Example: P-...)",
)
if err != nil {
return err
}
}
disableOwnerAddrID, err := address.ParseToIDs([]string{disableOwnerAddr})
if err != nil {
return fmt.Errorf("failure parsing disable owner address %s: %w", disableOwnerAddr, err)
}
disableOwners := warpMessage.PChainOwner{
Threshold: 1,
Addresses: disableOwnerAddrID,
}
extraAggregatorPeers, err := blockchain.GetAggregatorExtraPeers(app, clusterNameFlagValue, aggregatorExtraEndpoints)
if err != nil {
return err
}
aggregatorLogger, err := utils.NewLogger(
constants.SignatureAggregatorLogName,
aggregatorLogLevel,
constants.DefaultAggregatorLogLevel,
app.GetAggregatorLogDir(clusterNameFlagValue),
aggregatorLogToStdout,
ux.Logger.PrintToUser,
)
if err != nil {
return err
}
signedMessage, validationID, err := validatormanager.InitValidatorRegistration(
app,
network,
rpcURL,
chainSpec,
ownerPrivateKey,
nodeID,
blsInfo.PublicKey[:],
expiry,
remainingBalanceOwners,
disableOwners,
weight,
extraAggregatorPeers,
aggregatorAllowPrivatePeers,
aggregatorLogger,
pos,
delegationFee,
duration,
big.NewInt(int64(stakeAmount)),
validatorManagerAddress,
)
if err != nil {
return err
}
ux.Logger.PrintToUser("ValidationID: %s", validationID)
txID, _, err := deployer.RegisterL1Validator(balance, blsInfo, signedMessage)
if err != nil {
if !strings.Contains(err.Error(), "warp message already issued for validationID") {
return err
}
ux.Logger.PrintToUser(logging.LightBlue.Wrap("The Validation ID was already registered on the P-Chain. Proceeding to the next step"))
} else {
ux.Logger.PrintToUser("RegisterL1ValidatorTx ID: %s", txID)
if err := blockchain.UpdatePChainHeight(
"Waiting for P-Chain to update validator information ...",
); err != nil {
return err
}
}
if err := validatormanager.FinishValidatorRegistration(
app,
network,
rpcURL,
chainSpec,
ownerPrivateKey,
validationID,
extraAggregatorPeers,
aggregatorAllowPrivatePeers,
aggregatorLogger,
validatorManagerAddress,
); err != nil {
return err
}
ux.Logger.PrintToUser(" NodeID: %s", nodeID)
ux.Logger.PrintToUser(" Network: %s", network.Name())
// weight is inaccurate for PoS as it's fetched during registration
if !pos {
ux.Logger.PrintToUser(" Weight: %d", weight)
}
ux.Logger.PrintToUser(" Balance: %.2f", balanceAVAX)
ux.Logger.GreenCheckmarkToUser("Validator successfully added to the L1")
return nil
}
func CallAddValidatorNonSOV(
deployer *subnet.PublicDeployer,
network models.Network,
kc *keychain.Keychain,
useLedgerSetting bool,
blockchainName string,
nodeIDStr string,
defaultValidatorParamsSetting bool,
waitForTxAcceptanceSetting bool,
) error {
var start time.Time
nodeID, err := ids.NodeIDFromString(nodeIDStr)
if err != nil {
return err
}
useLedger = useLedgerSetting
defaultValidatorParams = defaultValidatorParamsSetting
waitForTxAcceptance = waitForTxAcceptanceSetting
if defaultValidatorParams {
useDefaultDuration = true
useDefaultStartTime = true
useDefaultWeight = true
}
if useDefaultDuration && duration != 0 {
return errMutuallyExclusiveDurationOptions
}
if useDefaultStartTime && startTimeStr != "" {
return errMutuallyExclusiveStartOptions
}
if useDefaultWeight && weight != 0 {
return errMutuallyExclusiveWeightOptions
}
if outputTxPath != "" {
if utils.FileExists(outputTxPath) {
return fmt.Errorf("outputTxPath %q already exists", outputTxPath)
}
}
_, err = ValidateSubnetNameAndGetChains([]string{blockchainName})
if err != nil {
return err
}
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return err
}
subnetID := sc.Networks[network.Name()].SubnetID
if subnetID == ids.Empty {
return errNoSubnetID
}
isPermissioned, controlKeys, threshold, err := txutils.GetOwners(network, subnetID)
if err != nil {
return err
}
if !isPermissioned {
return ErrNotPermissionedSubnet
}
kcKeys, err := kc.PChainFormattedStrAddresses()
if err != nil {
return err
}
// get keys for add validator tx signing
if subnetAuthKeys != nil {
if err := prompts.CheckSubnetAuthKeys(kcKeys, subnetAuthKeys, controlKeys, threshold); err != nil {
return err
}
} else {
subnetAuthKeys, err = prompts.GetSubnetAuthKeys(app.Prompt, kcKeys, controlKeys, threshold)
if err != nil {
return err
}
}
ux.Logger.PrintToUser("Your auth keys for add validator tx creation: %s", subnetAuthKeys)
selectedWeight, err := getWeight()
if err != nil {
return err
}
if selectedWeight < constants.MinStakeWeight {
return fmt.Errorf("invalid weight, must be greater than or equal to %d: %d", constants.MinStakeWeight, selectedWeight)
}
start, selectedDuration, err := getTimeParameters(network, nodeID, true)
if err != nil {
return err
}
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(selectedDuration).Format(constants.TimeParseLayout))
ux.Logger.PrintToUser("Weight: %d", selectedWeight)
ux.Logger.PrintToUser("Inputs complete, issuing transaction to add the provided validator information...")
isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.AddValidatorNonSOV(
waitForTxAcceptance,
controlKeys,
subnetAuthKeys,
subnetID,
nodeID,
selectedWeight,
start,
selectedDuration,
)
if err != nil {
return err
}
if !isFullySigned {
if err := SaveNotFullySignedTx(
"Add Validator",
tx,
blockchainName,
subnetAuthKeys,
remainingSubnetAuthKeys,
outputTxPath,
false,
); err != nil {
return err
}
}
return err
}
func PromptDuration(start time.Time, network models.Network, isPos bool) (time.Duration, error) {
for {
txt := "How long should this validator be validating? Enter a duration, e.g. 8760h. Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\""
var d time.Duration
var err error
switch {
case network.Kind == models.Fuji:
d, err = app.Prompt.CaptureFujiDuration(txt)
case network.Kind == models.Mainnet && isPos:
d, err = app.Prompt.CaptureMainnetL1StakingDuration(txt)
case network.Kind == models.Mainnet && !isPos:
d, err = app.Prompt.CaptureMainnetDuration(txt)
default:
d, err = app.Prompt.CaptureDuration(txt)
}
if err != nil {
return 0, err
}
end := start.Add(d)
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 d, nil
}
}
}
func getTimeParameters(network models.Network, nodeID ids.NodeID, isValidator bool) (time.Time, time.Duration, error) {
defaultStakingStartLeadTime := constants.StakingStartLeadTime
if network.Kind == models.Devnet {
defaultStakingStartLeadTime = constants.DevnetStakingStartLeadTime
}
const custom = "Custom"
// this sets either the global var startTimeStr or useDefaultStartTime to enable repeated execution with
// state keeping from node cmds
if startTimeStr == "" && !useDefaultStartTime {
if isValidator {
ux.Logger.PrintToUser("When should your validator start validating?\n" +
"If you validator is not ready by this time, subnet downtime can occur.")
} else {
ux.Logger.PrintToUser("When do you want to start delegating?\n")
}
defaultStartOption := "Start in " + ux.FormatDuration(defaultStakingStartLeadTime)
startTimeOptions := []string{defaultStartOption, custom}
startTimeOption, err := app.Prompt.CaptureList("Start time", startTimeOptions)
if err != nil {
return time.Time{}, 0, err
}
switch startTimeOption {
case defaultStartOption:
useDefaultStartTime = true
default:
start, err := promptStart()
if err != nil {
return time.Time{}, 0, err
}
startTimeStr = start.Format(constants.TimeParseLayout)
}
}
var (
err error
start time.Time
)
if startTimeStr != "" {
start, err = time.Parse(constants.TimeParseLayout, startTimeStr)
if err != nil {
return time.Time{}, 0, err
}
if start.Before(time.Now().Add(constants.StakingMinimumLeadTime)) {
return time.Time{}, 0, fmt.Errorf("time should be at least %s in the future ", constants.StakingMinimumLeadTime)
}
} else {
start = time.Now().Add(defaultStakingStartLeadTime)
}
// this sets either the global var duration or useDefaultDuration to enable repeated execution with
// state keeping from node cmds
if duration == 0 && !useDefaultDuration {
msg := "How long should your validator validate for?"
if !isValidator {
msg = "How long do you want to delegate for?"
}
const defaultDurationOption = "Until primary network validator expires"
durationOptions := []string{defaultDurationOption, custom}
durationOption, err := app.Prompt.CaptureList(msg, durationOptions)
if err != nil {
return time.Time{}, 0, err
}
switch durationOption {
case defaultDurationOption:
useDefaultDuration = true
default:
duration, err = PromptDuration(start, network, false) // notSoV
if err != nil {
return time.Time{}, 0, err
}
}
}
var selectedDuration time.Duration
if useDefaultDuration {
// avoid setting both globals useDefaultDuration and duration
selectedDuration, err = utils.GetRemainingValidationTime(network.Endpoint, nodeID, avagoconstants.PrimaryNetworkID, start)
if err != nil {
return time.Time{}, 0, err
}
} else {
selectedDuration = duration
}
return start, selectedDuration, nil
}
func promptStart() (time.Time, error) {
txt := "When should the validator start validating? Enter a UTC datetime in 'YYYY-MM-DD HH:MM:SS' format"
return app.Prompt.CaptureDate(txt)
}
func PromptNodeID(goal string) (ids.NodeID, error) {
txt := fmt.Sprintf("What is the NodeID of the node you want to %s?", goal)
return app.Prompt.CaptureNodeID(txt)
}
func getWeight() (uint64, error) {
// this sets either the global var weight or useDefaultWeight to enable repeated execution with
// state keeping from node cmds
if weight == 0 && !useDefaultWeight {
defaultWeight := fmt.Sprintf("Default (%d)", constants.DefaultStakeWeight)
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:
useDefaultWeight = true
default:
weight, err = app.Prompt.CaptureWeight(txt, func(uint64) error { return nil })
if err != nil {
return 0, err
}
}
}
if useDefaultWeight {
return constants.DefaultStakeWeight, nil
}
return weight, nil
}