diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index ca1beea00..c12f61fdf 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -21,9 +21,12 @@ jobs: "\\[Network\\]", "\\[Package Management\\]", "\\[Root\\]", - "\\[Local Subnet\\]", + "\\[Local Subnet non SOV\\]", "\\[Subnet Compatibility\\]", - "\\[Public Subnet\\]", + "\\[Public Subnet non SOV\\]", + "\\[Etna Subnet SOV\\]", + "\\[Etna AddRemove Validator SOV PoA\\]", + "\\[Etna AddRemove Validator SOV PoS\\]", "\\[Subnet\\]", "\\[Upgrade expect network failure\\]", "\\[Upgrade public network\\]", @@ -34,6 +37,8 @@ jobs: ] os: [ubuntu-latest, macos-14] exclude: + - os: ubuntu-latest + suite: "\\[Etna Subnet SOV\\]" - os: macos-14 suite: "\\[Node create\\]" - os: macos-14 @@ -41,7 +46,11 @@ jobs: - os: macos-14 suite: "\\[Docker\\]" - os: macos-14 - suite: "\\[Public Subnet\\]" + suite: "\\[Public Subnet non SOV\\]" + - os: ubuntu-latest + suite: "\\[Etna AddRemove Validator SOV PoA\\]" + - os: ubuntu-latest + suite: "\\[Etna AddRemove Validator SOV PoS\\]" steps: - name: Checkout repository uses: actions/checkout@v4 @@ -76,7 +85,7 @@ jobs: npm install -g tsx - name: Install Docker on MacOS - if: ${{ (matrix.os == 'macos-14') && (matrix.suite == '\\[Public Subnet\\]') }} + if: ${{ (matrix.os == 'macos-14') && (matrix.suite == '\\[Public Subnet non SOV\\]') }} run: | brew install docker brew install colima diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index d91dd4082..2371352ee 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -29,7 +29,6 @@ jobs: - name: Build and Test run: | scripts/build.sh - go test -v -coverprofile=coverage.out $(go list ./... | grep -v /tests/ | grep -v '/sdk/') - go tool cover -func=coverage.out + scripts/unit_test.sh env: CGO_CFLAGS: "-O -D__BLST_PORTABLE__" # Set the CGO flags to use the portable version of BLST diff --git a/.gitignore b/.gitignore index e14603067..184e0d0d3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +*.lcov # ignore GoLand metafiles directory .idea/ @@ -53,3 +54,4 @@ pkg/contract/contracts/cache/ pkg/contract/contracts/out/ osxcross/ + diff --git a/Dockerfile b/Dockerfile index 415ef9f6a..0db8043de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ============= Compilation Stage ================ -FROM golang:1.22.7-bullseye AS builder +FROM golang:1.22.8-bullseye AS builder WORKDIR /build # Copy and download avalanche dependencies using go mod diff --git a/cmd/backendcmd/spawn_server.go b/cmd/backendcmd/spawn_server.go index f01ee6960..81a9c7dda 100644 --- a/cmd/backendcmd/spawn_server.go +++ b/cmd/backendcmd/spawn_server.go @@ -13,12 +13,17 @@ import ( "github.com/spf13/cobra" ) -var app *application.Avalanche +var ( + app *application.Avalanche + serverPort string + gatewayPort string + snapshotsDir string +) // backendCmd is the command to run the backend gRPC process func NewCmd(injectedApp *application.Avalanche) *cobra.Command { app = injectedApp - return &cobra.Command{ + cmd := &cobra.Command{ Use: constants.BackendCmd, Short: "Run the backend server", Long: "This tool requires a backend process to run; this command starts it", @@ -26,10 +31,17 @@ func NewCmd(injectedApp *application.Avalanche) *cobra.Command { Args: cobrautils.ExactArgs(0), Hidden: true, } + cmd.Flags().StringVar(&serverPort, "server-port", binutils.LocalNetworkGRPCServerPort, "server port to use") + cmd.Flags().StringVar(&gatewayPort, "gateway-port", binutils.LocalNetworkGRPCGatewayPort, "gateway port to use") + cmd.Flags().StringVar(&snapshotsDir, "snapshots-dir", "", "snapshots dir to use") + return cmd } func startBackend(_ *cobra.Command, _ []string) error { - s, err := binutils.NewGRPCServer(app.GetSnapshotsDir()) + if snapshotsDir == "" { + snapshotsDir = app.GetSnapshotsDir() + } + s, err := binutils.NewGRPCServer(serverPort, gatewayPort, snapshotsDir) if err != nil { return err } diff --git a/cmd/blockchaincmd/add_validator.go b/cmd/blockchaincmd/add_validator.go index cca7eb589..2c0631e8d 100644 --- a/cmd/blockchaincmd/add_validator.go +++ b/cmd/blockchaincmd/add_validator.go @@ -3,12 +3,20 @@ package blockchaincmd import ( + "encoding/hex" "errors" "fmt" + "math/big" "time" + "github.com/ava-labs/avalanchego/utils/units" + + "github.com/ava-labs/avalanchego/api/info" + "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" @@ -17,9 +25,13 @@ import ( "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/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" "github.com/ava-labs/avalanchego/vms/platformvm" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/spf13/cobra" ) @@ -29,23 +41,35 @@ var ( networkoptions.Devnet, networkoptions.Fuji, networkoptions.Mainnet, - } - - nodeIDStr string - weight uint64 - startTimeStr string - duration time.Duration - defaultValidatorParams bool - useDefaultStartTime bool - useDefaultDuration bool - useDefaultWeight bool - waitForTxAcceptance bool + networkoptions.EtnaDevnet, + } + + nodeIDStr string + nodeEndpoint string + balance uint64 + 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 + 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 + clusterNameFlagValue string ) // avalanche blockchain addValidator @@ -69,44 +93,70 @@ Testnet or Mainnet.`, networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, true, addValidatorSupportedNetworkOptions) cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji/devnet only]") - cmd.Flags().StringVar(&nodeIDStr, "nodeID", "", "set the NodeID of the validator to add") - cmd.Flags().Uint64Var(&weight, "weight", 0, "set the staking weight of the validator to add") - - cmd.Flags().BoolVar(&useDefaultStartTime, "default-start-time", false, "use default start time for subnet validator (5 minutes later for fuji & mainnet, 30 seconds later for devnet)") - cmd.Flags().StringVar(&startTimeStr, "start-time", "", "UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format") - - cmd.Flags().BoolVar(&useDefaultDuration, "default-duration", false, "set duration so as to validate until primary validator ends its period") - cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long this validator will be staking") - - cmd.Flags().BoolVar(&defaultValidatorParams, "default-validator-params", false, "use default weight/start/duration params for subnet validator") - - cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "control keys that will be used to authenticate add validator tx") - cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "file path of the add validator tx") + cmd.Flags().Uint64Var(&weight, "weight", constants.NonBootstrapValidatorWeight, "set the staking weight of the validator to add") + cmd.Flags().Uint64Var(&balance, "balance", 0, "set the AVAX balance of the validator that will be used for continuous fee to 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().BoolVar(&waitForTxAcceptance, "wait-for-tx-acceptance", true, "just issue the add validator tx, without waiting for its acceptance") + 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().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") + 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", "Off", "log level to use with signature aggregator") + cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long this validator will be staking") + cmd.Flags().BoolVar(&useDefaultStartTime, "default-start-time", false, "(for non sovereign blockchain) 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 non sovereign blockchain) UTC start time when this validator starts validating, in 'YYYY-MM-DD HH:MM:SS' format") + cmd.Flags().BoolVar(&useDefaultDuration, "default-duration", false, "(for non sovereign blockchain) set duration so as to validate until primary validator ends its period") + cmd.Flags().BoolVar(&defaultValidatorParams, "default-validator-params", false, "(for non sovereign blockchain) use default weight/start/duration params for subnet validator") + cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "(for non sovereign blockchain) control keys that will be used to authenticate add validator tx") + cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "(for non sovereign blockchain) file path of the add validator tx") + cmd.Flags().BoolVar(&waitForTxAcceptance, "wait-for-tx-acceptance", true, "(for non sovereign blockchain) 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 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) + } + + networkOptionsList := networkoptions.GetNetworkFromSidecar(sc, addValidatorSupportedNetworkOptions) network, err := networkoptions.GetNetworkFromCmdLineFlags( app, "", globalNetworkFlags, true, false, - addValidatorSupportedNetworkOptions, + networkOptionsList, "", ) if err != nil { return err } + + if network.ClusterName != "" { + clusterNameFlagValue = network.ClusterName + network = models.ConvertClusterToNetwork(network) + } + fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.AddSubnetValidatorFee kc, err := keychain.GetKeychainFromCmdLineFlags( app, - constants.PayTxsFeesMsg, + "to pay for transaction fees on P-Chain", network, keyName, useEwoq, @@ -117,15 +167,264 @@ func addValidator(_ *cobra.Command, args []string) error { if err != nil { return err } - network.HandlePublicNetworkSimulation() - if err := UpdateKeychainWithSubnetControlKeys(kc, network, blockchainName); err != nil { + + if nodeEndpoint != "" { + infoClient := info.NewClient(nodeEndpoint) + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + nodeID, proofOfPossession, err := infoClient.GetNodeID(ctx) + if err != nil { + return err + } + nodeIDStr = nodeID.String() + publicKey = "0x" + hex.EncodeToString(proofOfPossession.PublicKey[:]) + pop = "0x" + hex.EncodeToString(proofOfPossession.ProofOfPossession[:]) + } + 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 } + + sovereign := sc.Sovereign + + if sovereign && network.Kind == models.Mainnet { + return errNotSupportedOnMainnet + } + + 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) - return CallAddValidator(deployer, network, kc, useLedger, blockchainName, nodeIDStr, defaultValidatorParams, waitForTxAcceptance) + if !sovereign { + return CallAddValidatorNonSOV(deployer, network, kc, useLedger, blockchainName, nodeIDStr, defaultValidatorParams, waitForTxAcceptance) + } + return CallAddValidator(deployer, network, kc, blockchainName, nodeIDStr, publicKey, pop) +} + +func promptValidatorBalance(availableBalance uint64) (uint64, 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) } func CallAddValidator( + deployer *subnet.PublicDeployer, + network models.Network, + kc *keychain.Keychain, + blockchainName string, + nodeIDStr string, + publicKey string, + pop string, +) error { + nodeID, err := ids.NodeIDFromString(nodeIDStr) + if err != nil { + return err + } + blsInfo, err := getBLSInfo(publicKey, pop) + if err != nil { + return fmt.Errorf("failure parsing BLS info: %w", err) + } + + expiry := uint64(time.Now().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) + } + 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) + 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 + } + } + + if balance == 0 { + availableBalance, err := utils.GetNetworkBalance(kc.Addresses().List(), network.Endpoint) + if err != nil { + return err + } + balanceAVAX, err := promptValidatorBalance(availableBalance) + if err != nil { + return err + } + // convert to nanoAVAX + balance = balanceAVAX * units.Avax + } + + if remainingBalanceOwnerAddr == "" { + remainingBalanceOwnerAddr, err = getKeyForChangeOwner(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 := GetAggregatorExtraPeers(clusterNameFlagValue, aggregatorExtraEndpoints) + if err != nil { + return err + } + signedMessage, validationID, err := validatormanager.InitValidatorRegistration( + app, + network, + rpcURL, + chainSpec, + ownerPrivateKey, + nodeID, + blsInfo.PublicKey[:], + expiry, + remainingBalanceOwners, + disableOwners, + weight, + extraAggregatorPeers, + aggregatorLogLevel, + pos, + delegationFee, + duration, + big.NewInt(int64(stakeAmount)), + ) + if err != nil { + return err + } + ux.Logger.PrintToUser("ValidationID: %s", validationID) + + txID, _, err := deployer.RegisterL1Validator(balance, blsInfo, signedMessage) + if err != nil { + return err + } + ux.Logger.PrintToUser("RegisterSubnetValidatorTx ID: %s", txID) + + if err := UpdatePChainHeight( + "Waiting for P-Chain to update validator information ...", + ); err != nil { + return err + } + + if err := validatormanager.FinishValidatorRegistration( + app, + network, + rpcURL, + chainSpec, + ownerPrivateKey, + validationID, + extraAggregatorPeers, + aggregatorLogLevel, + ); 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: %d", balance/units.Avax) + ux.Logger.GreenCheckmarkToUser("Validator successfully added to the Subnet") + + return nil +} + +func CallAddValidatorNonSOV( deployer *subnet.PublicDeployer, network models.Network, kc *keychain.Keychain, @@ -135,12 +434,11 @@ func CallAddValidator( defaultValidatorParamsSetting bool, waitForTxAcceptanceSetting bool, ) error { - var ( - nodeID ids.NodeID - start time.Time - err error - ) - + var start time.Time + nodeID, err := ids.NodeIDFromString(nodeIDStr) + if err != nil { + return err + } useLedger = useLedgerSetting defaultValidatorParams = defaultValidatorParamsSetting waitForTxAcceptance = waitForTxAcceptanceSetting @@ -208,24 +506,12 @@ func CallAddValidator( } ux.Logger.PrintToUser("Your subnet auth keys for add validator tx creation: %s", subnetAuthKeys) - if nodeIDStr == "" { - nodeID, err = PromptNodeID() - if err != nil { - return err - } - } else { - nodeID, err = ids.NodeIDFromString(nodeIDStr) - if err != nil { - return err - } - } - selectedWeight, err := getWeight() if err != nil { return err } if selectedWeight < constants.MinStakeWeight { - return fmt.Errorf("illegal weight, must be greater than or equal to %d: %d", constants.MinStakeWeight, selectedWeight) + return fmt.Errorf("invalid weight, must be greater than or equal to %d: %d", constants.MinStakeWeight, selectedWeight) } start, selectedDuration, err := getTimeParameters(network, nodeID, true) @@ -240,7 +526,7 @@ func CallAddValidator( ux.Logger.PrintToUser("Weight: %d", selectedWeight) ux.Logger.PrintToUser("Inputs complete, issuing transaction to add the provided validator information...") - isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.AddValidator( + isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.AddValidatorNonSOV( waitForTxAcceptance, controlKeys, subnetAuthKeys, @@ -275,10 +561,15 @@ func PromptDuration(start time.Time, network models.Network) (time.Duration, err 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 - if network.Kind == models.Fuji { + switch network.Kind { + case models.Fuji: d, err = app.Prompt.CaptureFujiDuration(txt) - } else { + case models.Mainnet: d, err = app.Prompt.CaptureMainnetDuration(txt) + case models.EtnaDevnet: + d, err = app.Prompt.CaptureEtnaDuration(txt) + default: + d, err = app.Prompt.CaptureDuration(txt) } if err != nil { return 0, err @@ -406,13 +697,8 @@ func promptStart() (time.Time, error) { return app.Prompt.CaptureDate(txt) } -func PromptNodeID() (ids.NodeID, error) { - ux.Logger.PrintToUser("Next, we need the NodeID of the validator you want to whitelist.") - ux.Logger.PrintToUser("") - ux.Logger.PrintToUser("Check https://docs.avax.network/apis/avalanchego/apis/info#infogetnodeid for instructions about how to query the NodeID from your node") - ux.Logger.PrintToUser("(Edit host IP address and port to match your deployment, if needed).") - - txt := "What is the NodeID of the validator you'd like to whitelist?" +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) } diff --git a/cmd/blockchaincmd/blockchain.go b/cmd/blockchaincmd/blockchain.go index 514f92e52..54989cd6c 100644 --- a/cmd/blockchaincmd/blockchain.go +++ b/cmd/blockchaincmd/blockchain.go @@ -60,5 +60,7 @@ manage your Blockchain configurations and live deployments.`, cmd.AddCommand(newValidatorsCmd()) // subnet changeOwner cmd.AddCommand(newChangeOwnerCmd()) + // subnet changeWeight + cmd.AddCommand(newChangeWeightCmd()) return cmd } diff --git a/cmd/blockchaincmd/change_owner.go b/cmd/blockchaincmd/change_owner.go index 9ff6c950e..737e0d8af 100644 --- a/cmd/blockchaincmd/change_owner.go +++ b/cmd/blockchaincmd/change_owner.go @@ -3,6 +3,7 @@ package blockchaincmd import ( + "errors" "fmt" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -95,6 +96,10 @@ func changeOwner(_ *cobra.Command, args []string) error { return err } + if sc.Sovereign { + return errors.New("avalanche blockchain changeOwner is not applicable to sovereign blockchains") + } + subnetID := sc.Networks[network.Name()].SubnetID if subnetID == ids.Empty { return errNoSubnetID diff --git a/cmd/blockchaincmd/change_weight.go b/cmd/blockchaincmd/change_weight.go new file mode 100644 index 000000000..a3fb5a949 --- /dev/null +++ b/cmd/blockchaincmd/change_weight.go @@ -0,0 +1,201 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package blockchaincmd + +import ( + "errors" + "fmt" + "os" + + "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/networkoptions" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/subnet" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/ids" + "github.com/spf13/cobra" +) + +// avalanche blockchain addValidator +func newChangeWeightCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "changeWeight [blockchainName]", + Short: "Changes the weight of a Subnet validator", + Long: `The blockchain changeWeight command changes the weight of a Subnet Validator. + +The Subnet has to be a Proof of Authority Subnet-Only Validator Subnet.`, + RunE: setWeight, + Args: cobrautils.ExactArgs(1), + } + networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, true, addValidatorSupportedNetworkOptions) + + cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji/devnet only]") + cmd.Flags().Uint64Var(&weight, "weight", constants.NonBootstrapValidatorWeight, "set the new staking weight of the validator") + cmd.Flags().BoolVarP(&useEwoq, "ewoq", "e", false, "use ewoq key [fuji/devnet only]") + cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "node-id of the validator") + 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") + return cmd +} + +func setWeight(_ *cobra.Command, args []string) error { + blockchainName := args[0] + err := prompts.ValidateNodeID(nodeIDStr) + if err != nil { + return err + } + + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return fmt.Errorf("failed to load sidecar: %w", err) + } + + networkOptionsList := networkoptions.GetNetworkFromSidecar(sc, removeValidatorSupportedNetworkOptions) + network, err := networkoptions.GetNetworkFromCmdLineFlags( + app, + "", + globalNetworkFlags, + true, + false, + networkOptionsList, + "", + ) + if err != nil { + return err + } + + if network.Kind == models.Mainnet && sc.Sovereign { + return errNotSupportedOnMainnet + } + + if outputTxPath != "" { + if _, err := os.Stat(outputTxPath); err == nil { + return fmt.Errorf("outputTxPath %q already exists", outputTxPath) + } + } + + if len(ledgerAddresses) > 0 { + useLedger = true + } + + if useLedger && keyName != "" { + return ErrMutuallyExlusiveKeyLedger + } + + switch network.Kind { + case models.Devnet: + if !useLedger && keyName == "" { + useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, constants.PayTxsFeesMsg, app.GetKeyDir(), false) + if err != nil { + return err + } + } + case models.Fuji: + if !useLedger && keyName == "" { + useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, constants.PayTxsFeesMsg, app.GetKeyDir(), false) + if err != nil { + return err + } + } + case models.Mainnet: + useLedger = true + if keyName != "" { + return ErrStoredKeyOnMainnet + } + default: + return errors.New("unsupported network") + } + + // get keychain accesor + fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.TxFee + kc, err := keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, fee) + if err != nil { + return err + } + + network.HandlePublicNetworkSimulation() + + subnetID := sc.Networks[network.Name()].SubnetID + if subnetID == ids.Empty { + return errNoSubnetID + } + + var nodeID ids.NodeID + if nodeIDStr == "" { + nodeID, err = PromptNodeID("add as a blockchain validator") + if err != nil { + return err + } + } else { + nodeID, err = ids.NodeIDFromString(nodeIDStr) + if err != nil { + return err + } + } + + isValidator, err := subnet.IsSubnetValidator(subnetID, nodeID, network) + if err != nil { + // just warn the user, don't fail + ux.Logger.PrintToUser("failed to check if node is a validator on the subnet: %s", err) + } else if !isValidator { + // this is actually an error + return fmt.Errorf("node %s is not a validator on subnet %s", nodeID, subnetID) + } + + deployer := subnet.NewPublicDeployer(app, kc, network) + + // first remove the validator from subnet + err = removeValidatorSOV(deployer, + network, + blockchainName, + nodeID, + false, // don't force + ) + if err != nil { + return err + } + + // TODO: we need to wait for the balance from the removed validator to arrive in changeAddr + // set arbitrary time.sleep here? + + weight, err = app.Prompt.CaptureWeight("What weight would you like to assign to the validator?") + if err != nil { + return err + } + + balance, err = getValidatorBalanceFromPChain() + if err != nil { + return err + } + + publicKey, pop, err = getBLSInfoFromPChain() + if err != nil { + return err + } + + remainingBalanceOwnerAddr, err = getChangeAddrFromPChain() + if err != nil { + return fmt.Errorf("failure parsing change owner address: %w", err) + } + + // add back validator to subnet with updated weight + return CallAddValidator(deployer, network, kc, blockchainName, nodeID.String(), publicKey, pop) +} + +// getValidatorBalanceFromPChain gets remaining balance of validator from p chain +func getValidatorBalanceFromPChain() (uint64, error) { + return 0, nil +} + +// getBLSInfoFromPChain gets BLS public key and pop from info api +func getBLSInfoFromPChain() (string, string, error) { + return "", "", nil +} + +// getChangeAddrFromPChain gets validator change addr from info api +func getChangeAddrFromPChain() (string, error) { + return "", nil +} diff --git a/cmd/blockchaincmd/create.go b/cmd/blockchaincmd/create.go index 44d6c7a46..9f969ae4f 100644 --- a/cmd/blockchaincmd/create.go +++ b/cmd/blockchaincmd/create.go @@ -45,6 +45,12 @@ type CreateFlags struct { useLatestReleasedVMVersion bool useLatestPreReleasedVMVersion bool useExternalGasToken bool + addICMRegistryToGenesis bool + proofOfStake bool + proofOfAuthority bool + rewardBasisPoints uint64 + validatorManagerOwner string + proxyContractOwner string enableDebugging bool } @@ -57,8 +63,10 @@ var ( errIllegalNameCharacter = errors.New( "illegal name character: only letters, no special characters allowed") - errMutuallyExlusiveVersionOptions = errors.New("version flags --latest,--pre-release,vm-version are mutually exclusive") - errMutuallyExclusiveVMConfigOptions = errors.New("--genesis flag disables --evm-chain-id,--evm-defaults,--production-defaults,--test-defaults") + errMutuallyExlusiveVersionOptions = errors.New("version flags --latest,--pre-release,vm-version are mutually exclusive") + errMutuallyExclusiveVMConfigOptions = errors.New("--genesis flag disables --evm-chain-id,--evm-defaults,--production-defaults,--test-defaults") + errMutuallyExlusiveValidatorManagementOptions = errors.New("validator management type flags --proof-of-authority,--proof-of-stake are mutually exclusive") + errSOVFlagsOnly = errors.New("flags --proof-of-authority, --proof-of-stake, --poa-manager-owner --proxy-contract-owner are only applicable to Subnet Only Validator (SOV) blockchains") ) // avalanche blockchain create @@ -102,7 +110,14 @@ configuration, pass the -f flag.`, cmd.Flags().BoolVar(&createFlags.useWarp, "warp", true, "generate a vm with warp support (needed for teleporter)") cmd.Flags().BoolVar(&createFlags.useTeleporter, "teleporter", false, "interoperate with other blockchains using teleporter") cmd.Flags().BoolVar(&createFlags.useExternalGasToken, "external-gas-token", false, "use a gas token from another blockchain") - cmd.Flags().BoolVar(&createFlags.enableDebugging, "debug", false, "enable blockchain debugging") + cmd.Flags().BoolVar(&createFlags.addICMRegistryToGenesis, "icm-registry-at-genesis", false, "setup ICM registry smart contract on genesis [experimental]") + cmd.Flags().BoolVar(&createFlags.proofOfAuthority, "proof-of-authority", false, "use proof of authority(PoA) for validator management") + cmd.Flags().BoolVar(&createFlags.proofOfStake, "proof-of-stake", false, "use proof of stake(PoS) for validator management") + cmd.Flags().StringVar(&createFlags.validatorManagerOwner, "validator-manager-owner", "", "EVM address that controls Validator Manager Owner") + cmd.Flags().StringVar(&createFlags.proxyContractOwner, "proxy-contract-owner", "", "EVM address that controls ProxyAdmin for TransparentProxy of ValidatorManager contract") + cmd.Flags().BoolVar(&sovereign, "sovereign", true, "set to false if creating non-sovereign blockchain") + cmd.Flags().Uint64Var(&createFlags.rewardBasisPoints, "reward-basis-points", 100, "(PoS only) reward basis points for PoS Reward Calculator") + cmd.Flags().BoolVar(&createFlags.enableDebugging, "debug", true, "enable blockchain debugging") return cmd } @@ -187,6 +202,20 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { return errors.New("flags --evm,--custom are mutually exclusive") } + if !sovereign { + if createFlags.proofOfAuthority || createFlags.proofOfStake || createFlags.validatorManagerOwner != "" || createFlags.proxyContractOwner != "" { + return errSOVFlagsOnly + } + } + // validator management type exclusiveness + if !flags.EnsureMutuallyExclusive([]bool{createFlags.proofOfAuthority, createFlags.proofOfStake}) { + return errMutuallyExlusiveValidatorManagementOptions + } + + if createFlags.rewardBasisPoints == 0 && createFlags.proofOfStake { + return fmt.Errorf("reward basis points cannot be zero") + } + // get vm kind vmType, err := vm.PromptVMType(app, createFlags.useSubnetEvm, createFlags.useCustomVM) if err != nil { @@ -195,7 +224,6 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { var ( genesisBytes []byte - sc *models.Sidecar useTeleporterFlag *bool deployTeleporter bool useExternalGasToken bool @@ -213,7 +241,33 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { return err } + sc := &models.Sidecar{} + + if sovereign { + if err = promptValidatorManagementType(app, sc); err != nil { + return err + } + } + if vmType == models.SubnetEvm { + if sovereign { + if createFlags.validatorManagerOwner == "" { + createFlags.validatorManagerOwner, err = getValidatorContractManagerAddr() + if err != nil { + return err + } + } + sc.ValidatorManagerOwner = createFlags.validatorManagerOwner + ux.Logger.GreenCheckmarkToUser("Validator Manager Contract owner address %s", createFlags.validatorManagerOwner) + + // use the validator manager owner as the transparent proxy contract owner unless specified via cmd flag + if createFlags.proxyContractOwner != "" { + sc.ProxyContractOwner = createFlags.proxyContractOwner + } else { + sc.ProxyContractOwner = sc.ValidatorManagerOwner + } + } + if genesisPath == "" { // Default defaultsKind, err = vm.PromptDefaults(app, defaultsKind) @@ -263,6 +317,7 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { var params vm.SubnetEVMGenesisParams params, tokenSymbol, err = vm.PromptSubnetEVMGenesisParams( app, + sc, vmVersion, createFlags.chainID, createFlags.tokenSymbol, @@ -278,22 +333,25 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { deployTeleporter = params.UseTeleporter useExternalGasToken = params.UseExternalGasToken genesisBytes, err = vm.CreateEVMGenesis( - blockchainName, params, teleporterInfo, + createFlags.addICMRegistryToGenesis, + sc.ProxyContractOwner, + createFlags.rewardBasisPoints, ) if err != nil { return err } } - sc, err = vm.CreateEvmSidecar( + if sc, err = vm.CreateEvmSidecar( + sc, app, blockchainName, vmVersion, tokenSymbol, true, - ) - if err != nil { + sovereign, + ); err != nil { return err } } else { @@ -318,7 +376,8 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { return err } } - sc, err = vm.CreateCustomSidecar( + if sc, err = vm.CreateCustomSidecar( + sc, app, blockchainName, useRepo, @@ -327,8 +386,8 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { customVMBuildScript, vmFile, tokenSymbol, - ) - if err != nil { + sovereign, + ); err != nil { return err } } @@ -385,6 +444,7 @@ func createBlockchainConfig(cmd *cobra.Command, args []string) error { } } ux.Logger.GreenCheckmarkToUser("Successfully created blockchain configuration") + ux.Logger.PrintToUser("Run 'avalanche blockchain describe' to view all created addresses and what their roles are") return nil } diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index 6c25c30ad..a34647686 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -1,19 +1,41 @@ -// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// / Copyright (C) 2022, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package blockchaincmd import ( + "encoding/hex" "encoding/json" "errors" "fmt" + "math/big" "os" "path/filepath" "strings" + "time" + + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/network/peer" + + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/keychain" + "github.com/ava-labs/avalanche-cli/pkg/node" + avagoutils "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanche-cli/pkg/binutils" "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/localnet" "github.com/ava-labs/avalanche-cli/pkg/metrics" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -36,32 +58,52 @@ import ( var deploySupportedNetworkOptions = []networkoptions.NetworkOption{ networkoptions.Local, networkoptions.Devnet, + networkoptions.EtnaDevnet, networkoptions.Fuji, networkoptions.Mainnet, } var ( - sameControlKey bool - keyName string - threshold uint32 - controlKeys []string - subnetAuthKeys []string - userProvidedAvagoVersion string - outputTxPath string - useLedger bool - useEwoq bool - ledgerAddresses []string - subnetIDStr string - mainnetChainID uint32 - skipCreatePrompt bool - avagoBinaryPath string - subnetOnly bool - icmSpec subnet.ICMSpec + sameControlKey bool + keyName string + threshold uint32 + controlKeys []string + subnetAuthKeys []string + userProvidedAvagoVersion string + outputTxPath string + useLedger bool + useLocalMachine bool + useEwoq bool + ledgerAddresses []string + sovereign bool + subnetIDStr string + mainnetChainID uint32 + skipCreatePrompt bool + avagoBinaryPath string + numBootstrapValidators int + numLocalNodes int + partialSync bool + changeOwnerAddress string + subnetOnly bool + icmSpec subnet.ICMSpec + generateNodeID bool + bootstrapValidatorsJSONFilePath string + privateKeyFlags contract.PrivateKeyFlags + bootstrapEndpoints []string + convertOnly bool + + poSMinimumStakeAmount uint64 + poSMaximumStakeAmount uint64 + poSMinimumStakeDuration uint64 + poSMinimumDelegationFee uint16 + poSMaximumStakeMultiplier uint8 + poSWeightToValueFactor uint64 errMutuallyExlusiveControlKeys = errors.New("--control-keys and --same-control-key are mutually exclusive") ErrMutuallyExlusiveKeyLedger = errors.New("key source flags --key, --ledger/--ledger-addrs are mutually exclusive") ErrStoredKeyOnMainnet = errors.New("key --key is not available for mainnet operations") errMutuallyExlusiveSubnetFlags = errors.New("--subnet-only and --subnet-id are mutually exclusive") + errNotSupportedOnMainnet = errors.New("deploying sovereign blockchain is currently not supported on Mainnet") ) // avalanche blockchain deploy @@ -84,6 +126,8 @@ so you can take your locally tested Subnet and deploy it on Fuji or Mainnet.`, Args: cobrautils.ExactArgs(1), } networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, true, deploySupportedNetworkOptions) + privateKeyFlags.SetFlagNames("blockchain-private-key", "blockchain-key", "blockchain-genesis-key") + privateKeyFlags.AddToCmd(cmd, "to fund validator manager initialization") cmd.Flags().StringVar(&userProvidedAvagoVersion, "avalanchego-version", "latest", "use this version of avalanchego (ex: v1.17.12)") cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji/devnet deploy only]") cmd.Flags().BoolVarP(&sameControlKey, "same-control-key", "s", false, "use the fee-paying key as control key") @@ -101,14 +145,53 @@ so you can take your locally tested Subnet and deploy it on Fuji or Mainnet.`, cmd.Flags().BoolVar(&icmSpec.SkipICMDeploy, "skip-local-teleporter", false, "skip automatic teleporter deploy on local networks [to be deprecated]") cmd.Flags().BoolVar(&icmSpec.SkipICMDeploy, "skip-teleporter-deploy", false, "skip automatic teleporter deploy") cmd.Flags().BoolVar(&icmSpec.SkipRelayerDeploy, "skip-relayer", false, "skip relayer deploy") - cmd.Flags().StringVar(&icmSpec.Version, "teleporter-version", "latest", "teleporter version to deploy") + cmd.Flags().StringVar(&icmSpec.ICMVersion, "teleporter-version", "latest", "teleporter version to deploy") + cmd.Flags().StringVar(&icmSpec.RelayerVersion, "relayer-version", "latest", "relayer version to deploy") cmd.Flags().StringVar(&icmSpec.MessengerContractAddressPath, "teleporter-messenger-contract-address-path", "", "path to an interchain messenger contract address file") cmd.Flags().StringVar(&icmSpec.MessengerDeployerAddressPath, "teleporter-messenger-deployer-address-path", "", "path to an interchain messenger deployer address file") cmd.Flags().StringVar(&icmSpec.MessengerDeployerTxPath, "teleporter-messenger-deployer-tx-path", "", "path to an interchain messenger deployer tx file") cmd.Flags().StringVar(&icmSpec.RegistryBydecodePath, "teleporter-registry-bytecode-path", "", "path to an interchain messenger registry bytecode file") + cmd.Flags().StringVar(&bootstrapValidatorsJSONFilePath, "bootstrap-filepath", "", "JSON file path that provides details about bootstrap validators, leave Node-ID and BLS values empty if using --generate-node-id=true") + cmd.Flags().BoolVar(&generateNodeID, "generate-node-id", false, "whether to create new node id for bootstrap validators (Node-ID and BLS values in bootstrap JSON file will be overridden if --bootstrap-filepath flag is used)") + cmd.Flags().StringSliceVar(&bootstrapEndpoints, "bootstrap-endpoints", nil, "take validator node info from the given endpoints") + cmd.Flags().BoolVar(&convertOnly, "convert-only", false, "avoid node track, restart and poa manager setup") + cmd.Flags().StringVar(&aggregatorLogLevel, "aggregator-log-level", "Off", "log level to use with signature aggregator") + cmd.Flags().StringSliceVar(&aggregatorExtraEndpoints, "aggregator-extra-endpoints", nil, "endpoints for extra nodes that are needed in signature aggregation") + cmd.Flags().BoolVar(&useLocalMachine, "use-local-machine", false, "use local machine as a blockchain validator") + cmd.Flags().IntVar(&numBootstrapValidators, "num-bootstrap-validators", 0, "(only if --generate-node-id is true) number of bootstrap validators to set up in sovereign L1 validator)") + cmd.Flags().IntVar(&numLocalNodes, "num-local-nodes", 5, "number of nodes to be created on local machine") + cmd.Flags().StringVar(&changeOwnerAddress, "change-owner-address", "", "address that will receive change if node is no longer L1 validator") + + cmd.Flags().Uint64Var(&poSMinimumStakeAmount, "pos-minimum-stake-amount", 1, "minimum stake amount") + cmd.Flags().Uint64Var(&poSMaximumStakeAmount, "pos-maximum-stake-amount", 1000, "maximum stake amount") + cmd.Flags().Uint64Var(&poSMinimumStakeDuration, "pos-minimum-stake-duration", 100, "minimum stake duration") + cmd.Flags().Uint16Var(&poSMinimumDelegationFee, "pos-minimum-delegation-fee", 1, "minimum delegation fee") + cmd.Flags().Uint8Var(&poSMaximumStakeMultiplier, "pos-maximum-stake-multiplier", 1, "maximum stake multiplier") + cmd.Flags().Uint64Var(&poSWeightToValueFactor, "pos-weight-to-value-factor", 1, "weight to value factor") + + cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "set primary network partial sync for new validators") return cmd } +type SubnetValidator struct { + // Must be Ed25519 NodeID + NodeID ids.NodeID `json:"nodeID"` + // Weight of this validator used when sampling + Weight uint64 `json:"weight"` + // When this validator will stop validating the Subnet + EndTime uint64 `json:"endTime"` + // Initial balance for this validator + Balance uint64 `json:"balance"` + // [Signer] is the BLS key for this validator. + // Note: We do not enforce that the BLS key is unique across all validators. + // This means that validators can share a key if they so choose. + // However, a NodeID + Subnet does uniquely map to a BLS key + Signer signer.Signer `json:"signer"` + // Leftover $AVAX from the [Balance] will be issued to this + // owner once it is removed from the validator set. + ChangeOwner fx.Owner `json:"changeOwner"` +} + func CallDeploy( cmd *cobra.Command, subnetOnlyParam bool, @@ -162,7 +245,7 @@ func getChainsInSubnet(blockchainName string) ([]string, error) { } func checkSubnetEVMDefaultAddressNotInAlloc(network models.Network, chain string) error { - if network.Kind != models.Local && network.Kind != models.Devnet && os.Getenv(constants.SimulatePublicNetwork) == "" { + if network.Kind != models.Local && network.Kind != models.Devnet && network.Kind != models.EtnaDevnet && os.Getenv(constants.SimulatePublicNetwork) == "" { genesis, err := app.LoadEvmGenesis(chain) if err != nil { return err @@ -280,6 +363,14 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } } + var bootstrapValidators []models.SubnetValidator + if bootstrapValidatorsJSONFilePath != "" { + bootstrapValidators, err = LoadBootstrapValidator(bootstrapValidatorsJSONFilePath) + if err != nil { + return err + } + } + chain := chains[0] sidecar, err := app.LoadSidecar(chain) @@ -297,6 +388,10 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } } + if !sidecar.Sovereign && bootstrapValidatorsJSONFilePath != "" { + return fmt.Errorf("--bootstrap-filepath flag is only applicable to sovereign blockchains") + } + network, err := networkoptions.GetNetworkFromCmdLineFlags( app, "", @@ -309,6 +404,13 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { if err != nil { return err } + clusterNameFlagValue = globalNetworkFlags.ClusterName + + if os.Getenv(constants.SimulatePublicNetwork) == "" { + if network.Kind == models.Mainnet && sidecar.Sovereign { + return errNotSupportedOnMainnet + } + } isEVMGenesis, validationErr, err := app.HasSubnetEVMGenesis(chain) if err != nil { @@ -373,10 +475,20 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } deployer := subnet.NewLocalDeployer(app, userProvidedAvagoVersion, avagoBinaryPath, vmBin, true) - deployInfo, err := deployer.DeployToLocalNetwork(chain, genesisPath, icmSpec, subnetIDStr) + deployInfo, err := deployer.DeployToLocalNetwork( + chain, + genesisPath, + icmSpec, + subnetIDStr, + constants.ServerRunFileLocalNetworkPrefix, + ) if err != nil { if deployer.BackendStartedHere() { - if innerErr := binutils.KillgRPCServerProcess(app); innerErr != nil { + if innerErr := binutils.KillgRPCServerProcess( + app, + binutils.LocalNetworkGRPCServerEndpoint, + constants.ServerRunFileLocalNetworkPrefix, + ); innerErr != nil { app.Log.Warn("tried to kill the gRPC server process but it failed", zap.Error(innerErr)) } } @@ -392,16 +504,14 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { deployInfo.BlockchainID, deployInfo.ICMMessengerAddress, deployInfo.ICMRegistryAddress, + nil, + clusterNameFlagValue, ); err != nil { return err } return PrintSubnetInfo(blockchainName, true) } - - // from here on we are assuming a public deploy - if subnetOnly && subnetIDStr != "" { - return errMutuallyExlusiveSubnetFlags - } + // end of local deploy createSubnet := true var subnetID ids.ID @@ -443,9 +553,164 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { return err } + if changeOwnerAddress == "" { + // use provided key as change owner unless already set + if pAddr, err := kc.PChainFormattedStrAddresses(); err == nil && len(pAddr) > 0 { + changeOwnerAddress = pAddr[0] + ux.Logger.PrintToUser("Using [%s] to be set as a change owner for leftover AVAX", changeOwnerAddress) + } + } + + if sidecar.Sovereign { + if !generateNodeID { + clusterName := fmt.Sprintf("%s-local-node", blockchainName) + if clusterNameFlagValue != "" { + clusterName = clusterNameFlagValue + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + // check if cluster is local + if clusterConfig.Local { + useLocalMachine = true + if len(bootstrapEndpoints) == 0 { + bootstrapEndpoints, err = getLocalBootstrapEndpoints() + if err != nil { + return fmt.Errorf("error getting local host bootstrap endpoints: %w, "+ + "please create your local node again and call subnet deploy command again", err) + } + } + network = models.ConvertClusterToNetwork(network) + } + } + // ask user if we want to use local machine if cluster is not provided + if !useLocalMachine && clusterNameFlagValue == "" { + ux.Logger.PrintToUser("You can use your local machine as a bootstrap validator on the blockchain") + ux.Logger.PrintToUser("This means that you don't have to to set up a remote server on a cloud service (e.g. AWS / GCP) to be a validator on the blockchain.") + + useLocalMachine, err = app.Prompt.CaptureYesNo("Do you want to use your local machine as a bootstrap validator?") + if err != nil { + return err + } + } + // if no cluster provided - we create one with fmt.Sprintf("%s-local-node", blockchainName) name + if useLocalMachine && clusterNameFlagValue == "" { + // stop local avalanchego process so that we can generate new local cluster + _ = node.StopLocalNode(app) + anrSettings := node.ANRSettings{} + avagoVersionSettings := node.AvalancheGoVersionSettings{} + useEtnaDevnet := network.Kind == models.EtnaDevnet + if avagoBinaryPath == "" { + useLatestAvalanchegoPreReleaseVersion := true + useLatestAvalanchegoReleaseVersion := false + if userProvidedAvagoVersion != "latest" { + useLatestAvalanchegoReleaseVersion = false + useLatestAvalanchegoPreReleaseVersion = false + } else { + userProvidedAvagoVersion = "" + } + avaGoVersionSetting := node.AvalancheGoVersionSettings{ + UseCustomAvalanchegoVersion: userProvidedAvagoVersion, + UseLatestAvalanchegoPreReleaseVersion: useLatestAvalanchegoPreReleaseVersion, + UseLatestAvalanchegoReleaseVersion: useLatestAvalanchegoReleaseVersion, + } + avalancheGoVersion, err := node.GetAvalancheGoVersion(app, avaGoVersionSetting) + if err != nil { + return err + } + _, avagoDir, err := binutils.SetupAvalanchego(app, avalancheGoVersion) + if err != nil { + return fmt.Errorf("failed installing Avalanche Go version %s: %w", avalancheGoVersion, err) + } + avagoBinaryPath = filepath.Join(avagoDir, "avalanchego") + } + nodeConfig := map[string]interface{}{} + if app.AvagoNodeConfigExists(blockchainName) { + nodeConfig, err = utils.ReadJSON(app.GetAvagoNodeConfigPath(blockchainName)) + if err != nil { + return err + } + } + // anrSettings, avagoVersionSettings, globalNetworkFlags are empty + if err = node.StartLocalNode( + app, + clusterName, + useEtnaDevnet, + avagoBinaryPath, + uint32(numLocalNodes), + partialSync, + nodeConfig, + anrSettings, + avagoVersionSettings, + globalNetworkFlags, + nil, + ); err != nil { + return err + } + clusterNameFlagValue = clusterName + if len(bootstrapEndpoints) == 0 { + bootstrapEndpoints, err = getLocalBootstrapEndpoints() + if err != nil { + return fmt.Errorf("error getting local host bootstrap endpoints: %w, "+ + "please create your local node again and call subnet deploy command again", err) + } + } + } + } + switch { + case len(bootstrapEndpoints) > 0: + if changeOwnerAddress == "" { + changeOwnerAddress, err = getKeyForChangeOwner(network) + if err != nil { + return err + } + } + for _, endpoint := range bootstrapEndpoints { + infoClient := info.NewClient(endpoint) + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + nodeID, proofOfPossession, err := infoClient.GetNodeID(ctx) + if err != nil { + return err + } + publicKey = "0x" + hex.EncodeToString(proofOfPossession.PublicKey[:]) + pop = "0x" + hex.EncodeToString(proofOfPossession.ProofOfPossession[:]) + + bootstrapValidators = append(bootstrapValidators, models.SubnetValidator{ + NodeID: nodeID.String(), + Weight: constants.BootstrapValidatorWeight, + Balance: constants.BootstrapValidatorBalance, + BLSPublicKey: publicKey, + BLSProofOfPossession: pop, + ChangeOwnerAddr: changeOwnerAddress, + }) + } + case clusterNameFlagValue != "": + // for remote clusters we don't need to ask for bootstrap validators and can read it from filesystem + bootstrapValidators, err = getClusterBootstrapValidators(clusterNameFlagValue, network) + if err != nil { + return fmt.Errorf("error getting bootstrap validators from cluster %s: %w", clusterNameFlagValue, err) + } + + default: + bootstrapValidators, err = promptBootstrapValidators(network, changeOwnerAddress, numBootstrapValidators) + if err != nil { + return err + } + } + } + + // from here on we are assuming a public deploy + if subnetOnly && subnetIDStr != "" { + return errMutuallyExlusiveSubnetFlags + } + network.HandlePublicNetworkSimulation() if createSubnet { + if sidecar.Sovereign { + sameControlKey = true + } controlKeys, threshold, err = promptOwners( kc, controlKeys, @@ -502,6 +767,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { if err != nil { return err } + deployer.CleanCacheWallet() // get the control keys in the same order as the tx _, controlKeys, threshold, err = txutils.GetOwners(network, subnetID) if err != nil { @@ -552,13 +818,272 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } } + if sidecar.Sovereign { + avaGoBootstrapValidators, err := ConvertToAvalancheGoSubnetValidator(bootstrapValidators) + if err != nil { + return err + } + deployer.CleanCacheWallet() + managerAddress := common.HexToAddress(validatorManagerSDK.ProxyContractAddress) + isFullySigned, convertL1TxID, tx, remainingSubnetAuthKeys, err := deployer.ConvertL1( + controlKeys, + subnetAuthKeys, + subnetID, + blockchainID, + managerAddress, + avaGoBootstrapValidators, + ) + if err != nil { + ux.Logger.RedXToUser("error converting blockchain: %s. fix the issue and try again with a new convert cmd", err) + return err + } + + savePartialTx = !isFullySigned && err == nil + ux.Logger.PrintToUser("ConvertL1Tx ID: %s", convertL1TxID) + + if savePartialTx { + if err := SaveNotFullySignedTx( + "ConvertL1Tx", + tx, + chain, + subnetAuthKeys, + remainingSubnetAuthKeys, + outputTxPath, + false, + ); err != nil { + return err + } + } + + _, err = ux.TimedProgressBar( + 30*time.Second, + "Waiting for L1 to be converted into sovereign blockchain ...", + 0, + ) + if err != nil { + return err + } + fmt.Println() + + if err := app.UpdateSidecarNetworks(&sidecar, network, subnetID, blockchainID, "", "", bootstrapValidators, clusterNameFlagValue); err != nil { + return err + } + + if !convertOnly && !generateNodeID { + clusterName := clusterNameFlagValue + if clusterName == "" { + clusterName, err = node.GetClusterNameFromList(app) + if err != nil { + return err + } + } + if !useLocalMachine { + if err = node.SyncSubnet(app, clusterName, blockchainName, true, nil); err != nil { + return err + } + + if err := node.WaitForHealthyCluster(app, clusterName, node.HealthCheckTimeout, node.HealthCheckPoolTime); err != nil { + return err + } + } else { + if err := node.TrackSubnetWithLocalMachine( + app, + clusterName, + blockchainName, + avagoBinaryPath, + ); err != nil { + return err + } + } + chainSpec := contract.ChainSpec{ + BlockchainName: blockchainName, + } + _, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + rpcURL, _, err := contract.GetBlockchainEndpoints( + app, + network, + chainSpec, + true, + false, + ) + if err != nil { + return err + } + client, err := evm.GetClient(rpcURL) + if err != nil { + return err + } + evm.WaitForChainID(client) + extraAggregatorPeers, err := GetAggregatorExtraPeers(clusterName, aggregatorExtraEndpoints) + if err != nil { + return err + } + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + blockchainID, err := contract.GetBlockchainID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + ownerAddress := common.HexToAddress(sidecar.ValidatorManagerOwner) + subnetSDK := blockchainSDK.Subnet{ + SubnetID: subnetID, + BlockchainID: blockchainID, + OwnerAddress: &ownerAddress, + RPC: rpcURL, + BootstrapValidators: avaGoBootstrapValidators, + } + logLvl, err := logging.ToLevel(aggregatorLogLevel) + if err != nil { + logLvl = logging.Off + } + if sidecar.ValidatorManagement == models.ProofOfStake { + ux.Logger.PrintToUser("Initializing Native Token Proof of Stake Validator Manager contract on blockchain %s ...", blockchainName) + if err := subnetSDK.InitializeProofOfStake( + network, + genesisPrivateKey, + extraAggregatorPeers, + logLvl, + validatorManagerSDK.PoSParams{ + MinimumStakeAmount: big.NewInt(int64(poSMinimumStakeAmount)), + MaximumStakeAmount: big.NewInt(int64(poSMaximumStakeAmount)), + MinimumStakeDuration: poSMinimumStakeDuration, + MinimumDelegationFee: poSMinimumDelegationFee, + MaximumStakeMultiplier: poSMaximumStakeMultiplier, + WeightToValueFactor: big.NewInt(int64(poSWeightToValueFactor)), + RewardCalculatorAddress: validatorManagerSDK.RewardCalculatorAddress, + }, + ); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("Proof of Stake Validator Manager contract successfully initialized on blockchain %s", blockchainName) + } else { + ux.Logger.PrintToUser("Initializing Proof of Authority Validator Manager contract on blockchain %s ...", blockchainName) + if err := subnetSDK.InitializeProofOfAuthority(network, genesisPrivateKey, extraAggregatorPeers, logLvl); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("Proof of Authority Validator Manager contract successfully initialized on blockchain %s", blockchainName) + } + } else { + ux.Logger.GreenCheckmarkToUser("Converted subnet successfully generated") + ux.Logger.PrintToUser("To finish conversion to sovereign L1, create the corresponding Avalanche node(s) with the provided Node ID and BLS Info") + ux.Logger.PrintToUser("Created Node ID and BLS Info can be found at %s", app.GetSidecarPath(blockchainName)) + ux.Logger.PrintToUser("Once the Avalanche Node(s) are created and are tracking the blockchain, call `avalanche contract initValidatorManager %s` to finish conversion to sovereign L1", blockchainName) + } + } else { + if err := app.UpdateSidecarNetworks(&sidecar, network, subnetID, blockchainID, "", "", nil, clusterNameFlagValue); err != nil { + return err + } + } flags := make(map[string]string) flags[constants.MetricsNetwork] = network.Name() metrics.HandleTracking(cmd, constants.MetricsSubnetDeployCommand, app, flags) // update sidecar // TODO: need to do something for backwards compatibility? - return app.UpdateSidecarNetworks(&sidecar, network, subnetID, blockchainID, "", "") + return nil +} + +func getClusterBootstrapValidators(clusterName string, network models.Network) ([]models.SubnetValidator, error) { + clusterConf, err := app.GetClusterConfig(clusterName) + if err != nil { + return nil, err + } + subnetValidators := []models.SubnetValidator{} + hostIDs := utils.Filter(clusterConf.GetCloudIDs(), clusterConf.IsAvalancheGoHost) + changeAddr := "" + for _, h := range hostIDs { + nodeID, pub, pop, err := utils.GetNodeParams(app.GetNodeInstanceDirPath(h)) + if err != nil { + return nil, fmt.Errorf("failed to parse nodeID: %w", err) + } + changeAddr, err = getKeyForChangeOwner(network) + if err != nil { + return nil, err + } + if err != nil { + return nil, err + } + ux.Logger.Info("Bootstrap validator info for Host: %s | Node ID: %s | Public Key: %s | Proof of Possession: %s", h, nodeID, hex.EncodeToString(pub), hex.EncodeToString(pop)) + subnetValidators = append(subnetValidators, models.SubnetValidator{ + NodeID: nodeID.String(), + Weight: constants.BootstrapValidatorWeight, + Balance: constants.BootstrapValidatorBalance, + BLSPublicKey: fmt.Sprintf("%s%s", "0x", hex.EncodeToString(pub)), + BLSProofOfPossession: fmt.Sprintf("%s%s", "0x", hex.EncodeToString(pop)), + ChangeOwnerAddr: changeAddr, + }) + } + return subnetValidators, nil +} + +func getBLSInfo(publicKey, proofOfPossesion string) (signer.ProofOfPossession, error) { + type jsonProofOfPossession struct { + PublicKey string + ProofOfPossession string + } + jsonPop := jsonProofOfPossession{ + PublicKey: publicKey, + ProofOfPossession: proofOfPossesion, + } + popBytes, err := json.Marshal(jsonPop) + if err != nil { + return signer.ProofOfPossession{}, err + } + pop := &signer.ProofOfPossession{} + err = pop.UnmarshalJSON(popBytes) + if err != nil { + return signer.ProofOfPossession{}, err + } + return *pop, nil +} + +// TODO: add deactivation owner? +func ConvertToAvalancheGoSubnetValidator(subnetValidators []models.SubnetValidator) ([]*txs.ConvertSubnetToL1Validator, error) { + bootstrapValidators := []*txs.ConvertSubnetToL1Validator{} + for _, validator := range subnetValidators { + nodeID, err := ids.NodeIDFromString(validator.NodeID) + if err != nil { + return nil, err + } + blsInfo, err := getBLSInfo(validator.BLSPublicKey, validator.BLSProofOfPossession) + if err != nil { + return nil, fmt.Errorf("failure parsing BLS info: %w", err) + } + addrs, err := address.ParseToIDs([]string{validator.ChangeOwnerAddr}) + if err != nil { + return nil, fmt.Errorf("failure parsing change owner address: %w", err) + } + bootstrapValidator := &txs.ConvertSubnetToL1Validator{ + NodeID: nodeID[:], + Weight: validator.Weight, + Balance: validator.Balance, + Signer: blsInfo, + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 1, + Addresses: addrs, + }, + } + bootstrapValidators = append(bootstrapValidators, bootstrapValidator) + } + avagoutils.Sort(bootstrapValidators) + return bootstrapValidators, nil } func ValidateSubnetNameAndGetChains(args []string) ([]string, error) { @@ -698,7 +1223,7 @@ func CheckForInvalidDeployAndGetAvagoVersion( } if networkRunning { - if userProvidedAvagoVersion == "latest" { + if userProvidedAvagoVersion == constants.LatestAvalancheGoVersion { if runningRPCVersion != configuredRPCVersion && !skipRPCCheck { return "", fmt.Errorf( "the current avalanchego deployment uses rpc version %d but your subnet has version %d and is not compatible", @@ -731,3 +1256,134 @@ func CheckForInvalidDeployAndGetAvagoVersion( } return desiredAvagoVersion, nil } + +func LoadBootstrapValidator(filepath string) ([]models.SubnetValidator, error) { + if !utils.FileExists(filepath) { + return nil, fmt.Errorf("file path %q doesn't exist", filepath) + } + jsonBytes, err := os.ReadFile(filepath) + if err != nil { + return nil, err + } + var subnetValidators []models.SubnetValidator + if err = json.Unmarshal(jsonBytes, &subnetValidators); err != nil { + return nil, err + } + if err = validateSubnetValidatorsJSON(generateNodeID, subnetValidators); err != nil { + return nil, err + } + if generateNodeID { + for _, subnetValidator := range subnetValidators { + subnetValidator.NodeID, subnetValidator.BLSPublicKey, subnetValidator.BLSProofOfPossession, err = generateNewNodeAndBLS() + if err != nil { + return nil, err + } + } + } + return subnetValidators, nil +} + +func UrisToPeers(uris []string) ([]info.Peer, error) { + peers := []info.Peer{} + ctx, cancel := utils.GetANRContext() + defer cancel() + for _, uri := range uris { + client := info.NewClient(uri) + nodeID, _, err := client.GetNodeID(ctx) + if err != nil { + return nil, err + } + ip, err := client.GetNodeIP(ctx) + if err != nil { + return nil, err + } + peers = append(peers, info.Peer{ + Info: peer.Info{ + ID: nodeID, + PublicIP: ip, + }, + }) + } + return peers, nil +} + +func ConvertURIToPeers(uris []string) ([]info.Peer, error) { + aggregatorPeers, err := UrisToPeers(uris) + if err != nil { + return nil, err + } + nodeIDs := utils.Map(aggregatorPeers, func(peer info.Peer) ids.NodeID { + return peer.Info.ID + }) + nodeIDsSet := set.Of(nodeIDs...) + for _, uri := range uris { + infoClient := info.NewClient(uri) + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + peers, err := infoClient.Peers(ctx, nil) + if err != nil { + return nil, err + } + for _, peer := range peers { + if !nodeIDsSet.Contains(peer.Info.ID) { + aggregatorPeers = append(aggregatorPeers, peer) + nodeIDsSet.Add(peer.Info.ID) + } + } + } + return aggregatorPeers, nil +} + +func GetAggregatorExtraPeers( + clusterName string, + extraURIs []string, +) ([]info.Peer, error) { + uris, err := GetAggregatorNetworkUris(clusterName) + if err != nil { + return nil, err + } + uris = append(uris, extraURIs...) + urisSet := set.Of(uris...) + uris = urisSet.List() + return ConvertURIToPeers(uris) +} + +func GetAggregatorNetworkUris(clusterName string) ([]string, error) { + aggregatorExtraPeerEndpointsUris := []string{} + if clusterName != "" { + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return nil, err + } + clusterConfig := clustersConfig.Clusters[clusterName] + if clusterConfig.Local { + cli, err := binutils.NewGRPCClientWithEndpoint( + binutils.LocalClusterGRPCServerEndpoint, + binutils.WithAvoidRPCVersionCheck(true), + binutils.WithDialTimeout(constants.FastGRPCDialTimeout), + ) + if err != nil { + return nil, err + } + ctx, cancel := utils.GetANRContext() + defer cancel() + status, err := cli.Status(ctx) + if err != nil { + return nil, err + } + for _, nodeInfo := range status.ClusterInfo.NodeInfos { + aggregatorExtraPeerEndpointsUris = append(aggregatorExtraPeerEndpointsUris, nodeInfo.Uri) + } + } else { // remote cluster case + hostIDs := utils.Filter(clusterConfig.GetCloudIDs(), clusterConfig.IsAvalancheGoHost) + for _, hostID := range hostIDs { + if nodeConfig, err := app.LoadClusterNodeConfig(hostID); err != nil { + return nil, err + } else { + aggregatorExtraPeerEndpointsUris = append(aggregatorExtraPeerEndpointsUris, fmt.Sprintf("http://%s:%d", nodeConfig.ElasticIP, constants.AvalanchegoAPIPort)) + } + } + } + } + return aggregatorExtraPeerEndpointsUris, nil +} diff --git a/cmd/blockchaincmd/describe.go b/cmd/blockchaincmd/describe.go index a8d3c93a0..df39e69bd 100644 --- a/cmd/blockchaincmd/describe.go +++ b/cmd/blockchaincmd/describe.go @@ -16,10 +16,12 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/localnet" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/subnet" + icmgenesis "github.com/ava-labs/avalanche-cli/pkg/teleporter/genesis" "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/vm" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" anr_utils "github.com/ava-labs/avalanche-network-runner/utils" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/logging" @@ -105,6 +107,7 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { } t.AppendRow(table.Row{"VM ID", vmIDstr, vmIDstr}, rowConfig) t.AppendRow(table.Row{"VM Version", sc.VMVersion, sc.VMVersion}, rowConfig) + t.AppendRow(table.Row{"Validation", sc.ValidatorManagement, sc.ValidatorManagement}, rowConfig) locallyDeployed, err := localnet.Deployed(sc.Name) if err != nil { @@ -112,10 +115,13 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { } localChainID := "" + blockchainID := "" for net, data := range sc.Networks { network, err := app.GetNetworkFromSidecarNetworkName(net) if err != nil { - return err + ux.Logger.RedXToUser("%s is supposed to be deployed to network %s: %s ", blockchainName, network.Name(), err) + ux.Logger.PrintToUser("") + continue } if network.Kind == models.Local && !locallyDeployed { continue @@ -154,10 +160,24 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { } } if data.BlockchainID != ids.Empty { + blockchainID = data.BlockchainID.String() hexEncoding := "0x" + hex.EncodeToString(data.BlockchainID[:]) t.AppendRow(table.Row{net, "BlockchainID (CB58)", data.BlockchainID.String()}) t.AppendRow(table.Row{net, "BlockchainID (HEX)", hexEncoding}) } + endpoint, _, err := contract.GetBlockchainEndpoints( + app, + network, + contract.ChainSpec{ + BlockchainName: sc.Name, + }, + false, + false, + ) + if err != nil { + return err + } + t.AppendRow(table.Row{net, "RPC Endpoint", endpoint}) } ux.Logger.PrintToUser(t.Render()) @@ -174,7 +194,7 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { for net, data := range sc.Networks { network, err := app.GetNetworkFromSidecarNetworkName(net) if err != nil { - return err + continue } if network.Kind == models.Local && !locallyDeployed { continue @@ -215,6 +235,7 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { if err := printAllocations(sc, genesis); err != nil { return err } + printSmartContracts(sc, genesis) printPrecompiles(genesis) } @@ -224,7 +245,7 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { return err } - localEndpoint := models.NewLocalNetwork().BlockchainEndpoint(sc.Name) + localEndpoint := models.NewLocalNetwork().BlockchainEndpoint(blockchainID) codespaceEndpoint, err := utils.GetCodespaceURL(localEndpoint) if err != nil { return err @@ -253,16 +274,14 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { func printAllocations(sc models.Sidecar, genesis core.Genesis) error { teleporterKeyAddress := "" - teleporterPrivKey := "" if sc.TeleporterReady { k, err := key.LoadSoft(models.NewLocalNetwork().ID, app.GetKeyPath(sc.TeleporterKey)) if err != nil { return err } teleporterKeyAddress = k.C() - teleporterPrivKey = k.PrivKeyHex() } - subnetAirdropKeyName, subnetAirdropAddress, subnetAirdropPrivKey, err := subnet.GetDefaultSubnetAirdropKeyInfo(app, sc.Name) + _, subnetAirdropAddress, _, err := subnet.GetDefaultSubnetAirdropKeyInfo(app, sc.Name) if err != nil { return err } @@ -273,22 +292,43 @@ func printAllocations(sc models.Sidecar, genesis core.Genesis) error { t.Style().Title.Format = text.FormatUpper t.Style().Options.SeparateRows = true t.SetTitle("Initial Token Allocation") - t.AppendHeader(table.Row{"Description", "Address and Private Key", "Amount (10^18)", "Amount (wei)"}) - for address := range genesis.Alloc { - amount := genesis.Alloc[address].Balance + t.AppendHeader(table.Row{ + "Description", + "Address and Private Key", + fmt.Sprintf("Amount (%s)", sc.TokenSymbol), + "Amount (wei)", + }) + for address, allocation := range genesis.Alloc { + amount := allocation.Balance + // we are only interested in supply distribution here + if amount == nil || big.NewInt(0).Cmp(amount) == 0 { + continue + } formattedAmount := new(big.Int).Div(amount, big.NewInt(params.Ether)) description := "" privKey := "" switch address.Hex() { case teleporterKeyAddress: - description = fmt.Sprintf("%s\n%s", sc.TeleporterKey, logging.Orange.Wrap("Teleporter Deploys")) - privKey = teleporterPrivKey + description = logging.Orange.Wrap("Used by ICM") case subnetAirdropAddress: - description = fmt.Sprintf("%s\n%s", subnetAirdropKeyName, logging.Orange.Wrap("Main funded account")) - privKey = subnetAirdropPrivKey + description = logging.Orange.Wrap("Main funded account") case vm.PrefundedEwoqAddress.Hex(): - description = "Main funded account EWOQ" - privKey = vm.PrefundedEwoqPrivate + description = logging.Orange.Wrap("Main funded account") + case sc.ValidatorManagerOwner: + description = logging.Orange.Wrap("Validator Manager Owner") + case sc.ProxyContractOwner: + description = logging.Orange.Wrap("Proxy Admin Owner") + } + var ( + found bool + name string + ) + found, name, _, privKey, err = contract.SearchForManagedKey(app, models.NewLocalNetwork(), address, true) + if err != nil { + return err + } + if found { + description = fmt.Sprintf("%s\n%s", description, name) } t.AppendRow(table.Row{description, address.Hex() + "\n" + privKey, formattedAmount.String(), amount.String()}) } @@ -297,6 +337,45 @@ func printAllocations(sc models.Sidecar, genesis core.Genesis) error { return nil } +func printSmartContracts(sc models.Sidecar, genesis core.Genesis) { + if len(genesis.Alloc) == 0 { + return + } + ux.Logger.PrintToUser("") + t := table.NewWriter() + t.Style().Title.Align = text.AlignCenter + t.Style().Title.Format = text.FormatUpper + t.Style().Options.SeparateRows = true + t.SetTitle("Smart Contracts") + t.AppendHeader(table.Row{"Description", "Address", "Deployer"}) + for address, allocation := range genesis.Alloc { + if len(allocation.Code) == 0 { + continue + } + var description, deployer string + switch { + case address == common.HexToAddress(icmgenesis.MessengerContractAddress): + description = "ICM Messenger" + deployer = icmgenesis.MessengerDeployerAddress + case address == common.HexToAddress(validatorManagerSDK.ValidatorContractAddress): + if sc.PoA() { + description = "PoA Validator Manager" + } else { + description = "Native Token Staking Manager" + } + case address == common.HexToAddress(validatorManagerSDK.ProxyContractAddress): + description = "Transparent Proxy" + case address == common.HexToAddress(validatorManagerSDK.ProxyAdminContractAddress): + description = "Proxy Admin" + deployer = sc.ProxyContractOwner + case address == common.HexToAddress(validatorManagerSDK.RewardCalculatorAddress): + description = "Reward Calculator" + } + t.AppendRow(table.Row{description, address.Hex(), deployer}) + } + ux.Logger.PrintToUser(t.Render()) +} + func printPrecompiles(genesis core.Genesis) { ux.Logger.PrintToUser("") t := table.NewWriter() diff --git a/cmd/blockchaincmd/export_test.go b/cmd/blockchaincmd/export_test.go index 4b9a43f28..474f928ec 100644 --- a/cmd/blockchaincmd/export_test.go +++ b/cmd/blockchaincmd/export_test.go @@ -38,11 +38,13 @@ func TestExportImportSubnet(t *testing.T) { genBytes, err := os.ReadFile("../../" + utils.SubnetEvmGenesisPath) require.NoError(err) sc, err := vm.CreateEvmSidecar( + nil, app, testSubnet, vmVersion, "Test", false, + true, ) require.NoError(err) err = app.WriteGenesisFile(testSubnet, genBytes) diff --git a/cmd/blockchaincmd/helpers.go b/cmd/blockchaincmd/helpers.go index 98ada06c5..8d1c24a4e 100644 --- a/cmd/blockchaincmd/helpers.go +++ b/cmd/blockchaincmd/helpers.go @@ -4,11 +4,16 @@ package blockchaincmd import ( "fmt" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/binutils" + "github.com/ava-labs/avalanche-cli/pkg/utils" "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/txutils" + "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/ids" "github.com/spf13/cobra" ) @@ -93,3 +98,36 @@ func UpdateKeychainWithSubnetControlKeys( } return nil } + +func UpdatePChainHeight( + title string, +) error { + _, err := ux.TimedProgressBar( + 30*time.Second, + title, + 0, + ) + if err != nil { + return err + } + fmt.Println() + return nil +} + +func getLocalBootstrapEndpoints() ([]string, error) { + ctx, cancel := utils.GetANRContext() + defer cancel() + cli, err := binutils.NewGRPCClientWithEndpoint(binutils.LocalClusterGRPCServerEndpoint) + if err != nil { + return nil, err + } + status, err := cli.Status(ctx) + if err != nil { + return nil, err + } + localBootstrapEndpoints := []string{} + for _, nodeInfo := range status.ClusterInfo.NodeInfos { + localBootstrapEndpoints = append(localBootstrapEndpoints, nodeInfo.Uri) + } + return localBootstrapEndpoints, nil +} diff --git a/cmd/blockchaincmd/join.go b/cmd/blockchaincmd/join.go index 4ce214aea..37ccb93f8 100644 --- a/cmd/blockchaincmd/join.go +++ b/cmd/blockchaincmd/join.go @@ -70,7 +70,7 @@ This command currently only supports Blockchains deployed on the Fuji Testnet an cmd.Flags().StringVar(&pluginDir, "plugin-dir", "", "file path of avalanchego's plugin directory") cmd.Flags().StringVar(&dataDir, "data-dir", "", "path of avalanchego's data dir directory") cmd.Flags().BoolVar(&printManual, "print", false, "if true, print the manual config without prompting") - cmd.Flags().StringVar(&nodeIDStr, "nodeID", "", "set the NodeID of the validator to check") + cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "set the NodeID of the validator to check") cmd.Flags().BoolVar(&forceWrite, "force-write", false, "if true, skip to prompt to overwrite the config file") cmd.Flags().Uint64Var(&stakeAmount, "stake-amount", 0, "amount of tokens to stake on validator") cmd.Flags().StringVar(&startTimeStr, "start-time", "", "start time that validator starts validating") @@ -98,6 +98,10 @@ func joinCmd(_ *cobra.Command, args []string) error { return err } + if sc.Sovereign { + return errors.New("avalanche blockchain join command cannot be used on sovereign blockchains") + } + network, err := networkoptions.GetNetworkFromCmdLineFlags( app, "", diff --git a/cmd/blockchaincmd/prompt_genesis_input.go b/cmd/blockchaincmd/prompt_genesis_input.go new file mode 100644 index 000000000..63d60bfa6 --- /dev/null +++ b/cmd/blockchaincmd/prompt_genesis_input.go @@ -0,0 +1,233 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package blockchaincmd + +import ( + "fmt" + + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "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/staking" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" +) + +func getValidatorContractManagerAddr() (string, error) { + return prompts.PromptAddress( + app.Prompt, + "enable as controller of ValidatorManager contract", + app.GetKeyDir(), + app.GetKey, + "", + models.UndefinedNetwork, + prompts.EVMFormat, + "Enter address", + ) +} + +func promptProofOfPossession(promptPublicKey, promptPop bool) (string, string, error) { + if promptPublicKey || promptPop { + ux.Logger.PrintToUser("Next, we need the public key and proof of possession of the node's BLS") + ux.Logger.PrintToUser("Check https://docs.avax.network/api-reference/info-api#infogetnodeid for instructions on calling info.getNodeID API") + } + var err error + publicKey := "" + proofOfPossesion := "" + if promptPublicKey { + txt := "What is the node's BLS public key?" + publicKey, err = app.Prompt.CaptureValidatedString(txt, prompts.ValidateHexa) + if err != nil { + return "", "", err + } + } + if promptPop { + txt := "What is the node's BLS proof of possession?" + proofOfPossesion, err = app.Prompt.CaptureValidatedString(txt, prompts.ValidateHexa) + if err != nil { + return "", "", err + } + } + return publicKey, proofOfPossesion, nil +} + +// TODO: add explain the difference for different validator management type +func promptValidatorManagementType( + app *application.Avalanche, + sidecar *models.Sidecar, +) error { + explainOption := "Explain the difference" + if createFlags.proofOfStake { + sidecar.ValidatorManagement = models.ProofOfStake + return nil + } + if createFlags.proofOfAuthority { + sidecar.ValidatorManagement = models.ProofOfAuthority + return nil + } + + options := []string{models.ProofOfAuthority, models.ProofOfStake, explainOption} + for { + option, err := app.Prompt.CaptureList( + "Which validator management type would you like to use in your blockchain?", + options, + ) + if err != nil { + return err + } + switch option { + case models.ProofOfAuthority: + sidecar.ValidatorManagement = models.ValidatorManagementTypeFromString(option) + case models.ProofOfStake: + sidecar.ValidatorManagement = models.ValidatorManagementTypeFromString(option) + case explainOption: + continue + } + break + } + return nil +} + +// generateNewNodeAndBLS returns node id, bls public key and bls pop +func generateNewNodeAndBLS() (string, string, string, error) { + certBytes, _, err := staking.NewCertAndKeyBytes() + if err != nil { + return "", "", "", err + } + nodeID, err := utils.ToNodeID(certBytes) + if err != nil { + return "", "", "", err + } + blsSignerKey, err := bls.NewSecretKey() + if err != nil { + return "", "", "", err + } + p := signer.NewProofOfPossession(blsSignerKey) + publicKey, err := formatting.Encode(formatting.HexNC, p.PublicKey[:]) + if err != nil { + return "", "", "", err + } + pop, err := formatting.Encode(formatting.HexNC, p.ProofOfPossession[:]) + if err != nil { + return "", "", "", err + } + return nodeID.String(), publicKey, pop, nil +} + +func promptBootstrapValidators(network models.Network, changeOwnerAddress string, numBootstrapValidators int) ([]models.SubnetValidator, error) { + var subnetValidators []models.SubnetValidator + var err error + if numBootstrapValidators == 0 { + numBootstrapValidators, err = app.Prompt.CaptureInt( + "How many bootstrap validators do you want to set up?", + prompts.ValidatePositiveInt, + ) + } + if err != nil { + return nil, err + } + var setUpNodes bool + if generateNodeID { + setUpNodes = false + } else { + setUpNodes, err = promptSetUpNodes() + if err != nil { + return nil, err + } + generateNodeID = !setUpNodes + } + if changeOwnerAddress == "" { + changeOwnerAddress, err = getKeyForChangeOwner(network) + if err != nil { + return nil, err + } + } + for len(subnetValidators) < numBootstrapValidators { + ux.Logger.PrintToUser("Getting info for bootstrap validator %d", len(subnetValidators)+1) + var nodeID ids.NodeID + var publicKey, pop string + if setUpNodes { + nodeID, err = PromptNodeID("add as bootstrap validator") + if err != nil { + return nil, err + } + publicKey, pop, err = promptProofOfPossession(true, true) + if err != nil { + return nil, err + } + } else { + nodeIDStr, publicKey, pop, err = generateNewNodeAndBLS() + if err != nil { + return nil, err + } + nodeID, err = ids.NodeIDFromString(nodeIDStr) + if err != nil { + return nil, err + } + } + subnetValidator := models.SubnetValidator{ + NodeID: nodeID.String(), + Weight: constants.BootstrapValidatorWeight, + Balance: constants.BootstrapValidatorBalance, + BLSPublicKey: publicKey, + BLSProofOfPossession: pop, + ChangeOwnerAddr: changeOwnerAddress, + } + subnetValidators = append(subnetValidators, subnetValidator) + ux.Logger.GreenCheckmarkToUser("Bootstrap Validator %d:", len(subnetValidators)) + ux.Logger.PrintToUser("- Node ID: %s", nodeID) + ux.Logger.PrintToUser("- Change Address: %s", changeOwnerAddress) + } + return subnetValidators, nil +} + +func validateBLS(publicKey, pop string) error { + if err := prompts.ValidateHexa(publicKey); err != nil { + return fmt.Errorf("format error in given public key: %w", err) + } + if err := prompts.ValidateHexa(pop); err != nil { + return fmt.Errorf("format error in given proof of possession: %w", err) + } + return nil +} + +func validateSubnetValidatorsJSON(generateNewNodeID bool, validatorJSONS []models.SubnetValidator) error { + for _, validatorJSON := range validatorJSONS { + if !generateNewNodeID { + if validatorJSON.NodeID == "" || validatorJSON.BLSPublicKey == "" || validatorJSON.BLSProofOfPossession == "" { + return fmt.Errorf("no Node ID or BLS info provided, use --generate-node-id flag to generate new Node ID and BLS info") + } + _, err := ids.NodeIDFromString(validatorJSON.NodeID) + if err != nil { + return fmt.Errorf("invalid node id %s", validatorJSON.NodeID) + } + if err = validateBLS(validatorJSON.BLSPublicKey, validatorJSON.BLSProofOfPossession); err != nil { + return err + } + } + if validatorJSON.Weight == 0 { + return fmt.Errorf("bootstrap validator weight has to be greater than 0") + } + if validatorJSON.Balance == 0 { + return fmt.Errorf("bootstrap validator balance has to be greater than 0") + } + } + return nil +} + +// promptProvideNodeID returns false if user doesn't have any Avalanche node set up yet to be +// bootstrap validators +func promptSetUpNodes() (bool, error) { + ux.Logger.PrintToUser("If you have set up your own Avalanche Nodes, you can provide the Node ID and BLS Key from those nodes in the next step.") + ux.Logger.PrintToUser("Otherwise, we will generate new Node IDs and BLS Key for you.") + setUpNodes, err := app.Prompt.CaptureYesNo("Have you set up your own Avalanche Nodes?") + if err != nil { + return false, err + } + return setUpNodes, nil +} diff --git a/cmd/blockchaincmd/prompt_owners.go b/cmd/blockchaincmd/prompt_owners.go index 5233890af..1c7f59a4e 100644 --- a/cmd/blockchaincmd/prompt_owners.go +++ b/cmd/blockchaincmd/prompt_owners.go @@ -53,7 +53,7 @@ func promptOwners( return nil, 0, fmt.Errorf("user cancelled operation") } } - ux.Logger.PrintToUser("Your Subnet's control keys: %s", controlKeys) + ux.Logger.PrintToUser("Your blockchain control keys: %s", controlKeys) // validate and prompt for threshold if threshold == 0 && subnetAuthKeys != nil { threshold = uint32(len(subnetAuthKeys)) @@ -289,3 +289,46 @@ func getThreshold(maxLen int) (uint32, error) { } return uint32(intTh), err } + +func getKeyForChangeOwner(network models.Network) (string, error) { + changeAddrPrompt := "Which key would you like to set as change owner for leftover AVAX if the node is removed from validator set?" + + const ( + getFromStored = "Get address from an existing stored key (created from avalanche key create or avalanche key import)" + custom = "Custom" + ) + + listOptions := []string{getFromStored, custom} + listDecision, err := app.Prompt.CaptureList(changeAddrPrompt, listOptions) + if err != nil { + return "", err + } + + var key string + + switch listDecision { + case getFromStored: + key, err = prompts.CaptureKeyAddress( + app.Prompt, + "be set as a change owner for leftover AVAX", + app.GetKeyDir(), + app.GetKey, + network, + prompts.PChainFormat, + ) + if err != nil { + return "", err + } + case custom: + addrPrompt := "Enter change address (P-chain format)" + changeAddr, err := app.Prompt.CaptureAddress(addrPrompt) + if err != nil { + return "", err + } + key = changeAddr.String() + } + if err != nil { + return "", err + } + return key, nil +} diff --git a/cmd/blockchaincmd/remove_validator.go b/cmd/blockchaincmd/remove_validator.go index ab5af308e..8a093c2ef 100644 --- a/cmd/blockchaincmd/remove_validator.go +++ b/cmd/blockchaincmd/remove_validator.go @@ -7,15 +7,21 @@ import ( "fmt" "os" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/utils/logging" + "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/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/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -27,6 +33,7 @@ var removeValidatorSupportedNetworkOptions = []networkoptions.NetworkOption{ networkoptions.Devnet, networkoptions.Fuji, networkoptions.Mainnet, + networkoptions.EtnaDevnet, } // avalanche blockchain removeValidator @@ -44,91 +51,250 @@ these prompts by providing the values with flags.`, } networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, removeValidatorSupportedNetworkOptions) cmd.Flags().StringVarP(&keyName, "key", "k", "", "select the key to use [fuji deploy only]") - cmd.Flags().StringVar(&nodeIDStr, "nodeID", "", "set the NodeID of the validator to remove") - cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "control keys that will be used to authenticate the removeValidator tx") - cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "file path of the removeValidator tx") + cmd.Flags().StringSliceVar(&subnetAuthKeys, "subnet-auth-keys", nil, "(for non-SOV blockchain only) control keys that will be used to authenticate the removeValidator tx") + cmd.Flags().StringVar(&outputTxPath, "output-tx-path", "", "(for non-SOV blockchain only) file path of the removeValidator tx") cmd.Flags().BoolVarP(&useLedger, "ledger", "g", false, "use ledger instead of key (always true on mainnet, defaults to false on fuji)") cmd.Flags().StringSliceVar(&ledgerAddresses, "ledger-addrs", []string{}, "use the given ledger addresses") + cmd.Flags().StringVar(&nodeIDStr, "node-id", "", "node-id of the validator") + cmd.Flags().StringVar(&nodeEndpoint, "node-endpoint", "", "remove validator that responds to the given endpoint") + cmd.Flags().StringSliceVar(&aggregatorExtraEndpoints, "aggregator-extra-endpoints", nil, "endpoints for extra nodes that are needed in signature aggregation") + privateKeyFlags.AddToCmd(cmd, "to pay fees for completing the validator's removal (blockchain gas token)") + cmd.Flags().StringVar(&rpcURL, "rpc", "", "connect to validator manager at the given rpc endpoint") + cmd.Flags().StringVar(&aggregatorLogLevel, "aggregator-log-level", "Off", "log level to use with signature aggregator") return cmd } func removeValidator(_ *cobra.Command, args []string) error { - var ( - nodeID ids.NodeID - err error - ) + blockchainName := args[0] + _, err := ValidateSubnetNameAndGetChains([]string{blockchainName}) + if err != nil { + return err + } + + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return err + } + networkOptionsList := networkoptions.GetNetworkFromSidecar(sc, removeValidatorSupportedNetworkOptions) network, err := networkoptions.GetNetworkFromCmdLineFlags( app, "", globalNetworkFlags, true, false, - removeValidatorSupportedNetworkOptions, + networkOptionsList, "", ) if err != nil { return err } + if network.ClusterName != "" { + network = models.ConvertClusterToNetwork(network) + } + fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.TxFee + kc, err := keychain.GetKeychainFromCmdLineFlags( + app, + "to pay for transaction fees on P-Chain", + network, + keyName, + useEwoq, + useLedger, + ledgerAddresses, + fee, + ) + if err != nil { + return err + } + network.HandlePublicNetworkSimulation() + + if sc.Sovereign && network.Kind == models.Mainnet { + return errNotSupportedOnMainnet + } + + if !sc.Sovereign { + if outputTxPath != "" { + return errors.New("--output-tx-path flag cannot be used for non-SOV (Subnet-Only Validators) blockchains") + } + if len(subnetAuthKeys) > 0 { + return errors.New("--subnetAuthKeys flag cannot be used for non-SOV (Subnet-Only Validators) blockchains") + } + } if outputTxPath != "" { if _, err := os.Stat(outputTxPath); err == nil { return fmt.Errorf("outputTxPath %q already exists", outputTxPath) } } - if len(ledgerAddresses) > 0 { - useLedger = true + var nodeID ids.NodeID + switch { + case nodeEndpoint != "": + infoClient := info.NewClient(nodeEndpoint) + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + nodeID, _, err = infoClient.GetNodeID(ctx) + if err != nil { + return err + } + case nodeIDStr == "": + nodeID, err = PromptNodeID("remove as a blockchain validator") + if err != nil { + return err + } + default: + nodeID, err = ids.NodeIDFromString(nodeIDStr) + if err != nil { + return err + } + } + + if network.Kind == models.Local && !sc.Sovereign { + return removeFromLocalNonSOV(blockchainName, nodeID) } - if useLedger && keyName != "" { - return ErrMutuallyExlusiveKeyLedger + scNetwork := sc.Networks[network.Name()] + subnetID := scNetwork.SubnetID + if subnetID == ids.Empty { + return errNoSubnetID } - chains, err := ValidateSubnetNameAndGetChains(args) - if err != nil { - return err + deployer := subnet.NewPublicDeployer(app, kc, network) + // check that this guy actually is a validator on the subnet + if !sc.Sovereign { + isValidator, err := subnet.IsSubnetValidator(subnetID, nodeID, network) + if err != nil { + // just warn the user, don't fail + ux.Logger.PrintToUser("failed to check if node is a validator on the subnet: %s", err) + } else if !isValidator { + // this is actually an error + return fmt.Errorf("node %s is not a validator on subnet %s", nodeID, subnetID) + } + if err := UpdateKeychainWithSubnetControlKeys(kc, network, blockchainName); err != nil { + return err + } + return removeValidatorNonSOV(deployer, network, subnetID, kc, blockchainName, nodeID) } - blockchainName := chains[0] - - switch network.Kind { - case models.Local: - return removeFromLocal(blockchainName) - case models.Fuji: - if !useLedger && keyName == "" { - useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, constants.PayTxsFeesMsg, app.GetKeyDir(), false) - if err != nil { - return err - } + // check if node is a bootstrap validator to force it to be removed + filteredBootstrapValidators := utils.Filter(scNetwork.BootstrapValidators, func(b models.SubnetValidator) bool { + if id, err := ids.NodeIDFromString(b.NodeID); err == nil && id == nodeID { + return true } - case models.Mainnet: - useLedger = true - if keyName != "" { - return ErrStoredKeyOnMainnet + return false + }) + force := len(filteredBootstrapValidators) > 0 + if err := removeValidatorSOV(deployer, network, blockchainName, nodeID, force); err != nil { + return err + } + // remove the validator from the list of bootstrap validators + newBootstrapValidators := utils.Filter(scNetwork.BootstrapValidators, func(b models.SubnetValidator) bool { + if id, _ := ids.NodeIDFromString(b.NodeID); id != nodeID { + return true } - default: - return errors.New("unsupported network") + return false + }) + // save new bootstrap validators and save sidecar + scNetwork.BootstrapValidators = newBootstrapValidators + sc.Networks[network.Name()] = scNetwork + if err := app.UpdateSidecar(&sc); err != nil { + return err } + return nil +} - // get keychain accesor - fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.TxFee - kc, err := keychain.GetKeychain(app, false, useLedger, ledgerAddresses, keyName, network, fee) +func removeValidatorSOV( + deployer *subnet.PublicDeployer, + network models.Network, + blockchainName string, + nodeID ids.NodeID, + force bool, +) error { + chainSpec := contract.ChainSpec{ + BlockchainName: blockchainName, + } + + sc, err := app.LoadSidecar(chainSpec.BlockchainName) + if err != nil { + return fmt.Errorf("failed to load sidecar: %w", err) + } + ownerPrivateKeyFound, _, _, ownerPrivateKey, err := contract.SearchForManagedKey( + app, + network, + common.HexToAddress(sc.ValidatorManagerOwner), + true, + ) + if err != nil { + return err + } + if !ownerPrivateKeyFound { + return fmt.Errorf("not private key found for Validator manager owner %s", sc.ValidatorManagerOwner) + } + ux.Logger.PrintToUser(logging.Yellow.Wrap("Validator manager owner %s pays for the initialization of the validator's removal (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) + clusterName := sc.Networks[network.Name()].ClusterName + extraAggregatorPeers, err := GetAggregatorExtraPeers(clusterName, aggregatorExtraEndpoints) if err != nil { return err } + if force && sc.PoS() { + ux.Logger.PrintToUser(logging.Yellow.Wrap("Forcing removal of %s as it is a PoS bootstrap validator"), nodeID) + } - network.HandlePublicNetworkSimulation() + signedMessage, validationID, err := validatormanager.InitValidatorRemoval( + app, + network, + rpcURL, + chainSpec, + ownerPrivateKey, + nodeID, + extraAggregatorPeers, + aggregatorLogLevel, + sc.PoS(), + force, + ) + if err != nil { + return err + } + ux.Logger.PrintToUser("ValidationID: %s", validationID) - sc, err := app.LoadSidecar(blockchainName) + txID, _, err := deployer.SetL1ValidatorWeight(signedMessage) if err != nil { return err } + ux.Logger.PrintToUser("SetSubnetValidatorWeightTx ID: %s", txID) - subnetID := sc.Networks[network.Name()].SubnetID - if subnetID == ids.Empty { - return errNoSubnetID + if err := validatormanager.FinishValidatorRemoval( + app, + network, + rpcURL, + chainSpec, + ownerPrivateKey, + validationID, + extraAggregatorPeers, + aggregatorLogLevel, + ); err != nil { + return err } + ux.Logger.GreenCheckmarkToUser("Validator successfully removed from the Subnet") + + return nil +} + +func removeValidatorNonSOV(deployer *subnet.PublicDeployer, network models.Network, subnetID ids.ID, kc *keychain.Keychain, blockchainName string, nodeID ids.NodeID) error { isPermissioned, controlKeys, threshold, err := txutils.GetOwners(network, subnetID) if err != nil { return err @@ -160,33 +326,10 @@ func removeValidator(_ *cobra.Command, args []string) error { } ux.Logger.PrintToUser("Your subnet auth keys for remove validator tx creation: %s", subnetAuthKeys) - if nodeIDStr == "" { - nodeID, err = PromptNodeID() - if err != nil { - return err - } - } else { - nodeID, err = ids.NodeIDFromString(nodeIDStr) - if err != nil { - return err - } - } - - // check that this guy actually is a validator on the subnet - isValidator, err := subnet.IsSubnetValidator(subnetID, nodeID, network) - if err != nil { - // just warn the user, don't fail - ux.Logger.PrintToUser("failed to check if node is a validator on the subnet: %s", err) - } else if !isValidator { - // this is actually an error - return fmt.Errorf("node %s is not a validator on subnet %s", nodeID, subnetID) - } - ux.Logger.PrintToUser("NodeID: %s", nodeID.String()) ux.Logger.PrintToUser("Network: %s", network.Name()) ux.Logger.PrintToUser("Inputs complete, issuing transaction to remove the specified validator...") - deployer := subnet.NewPublicDeployer(app, kc, network) isFullySigned, tx, remainingSubnetAuthKeys, err := deployer.RemoveValidator( controlKeys, subnetAuthKeys, @@ -209,11 +352,13 @@ func removeValidator(_ *cobra.Command, args []string) error { return err } } - return err } -func removeFromLocal(blockchainName string) error { +func removeFromLocalNonSOV( + blockchainName string, + nodeID ids.NodeID, +) error { sc, err := app.LoadSidecar(blockchainName) if err != nil { return err @@ -236,19 +381,6 @@ func removeFromLocal(blockchainName string) error { validatorList[i] = v.NodeID.String() } - if nodeIDStr == "" { - nodeIDStr, err = app.Prompt.CaptureList("Choose a validator to remove", validatorList) - if err != nil { - return err - } - } - - // Convert NodeID string to NodeID type - nodeID, err := ids.NodeIDFromString(nodeIDStr) - if err != nil { - return err - } - testKey := genesis.EWOQKey keyChain := secp256k1fx.NewKeychain(testKey) _, err = subnet.IssueRemoveSubnetValidatorTx(keyChain, subnetID, nodeID) diff --git a/cmd/blockchaincmd/upgradecmd/apply.go b/cmd/blockchaincmd/upgradecmd/apply.go index 207e05f9b..2df242a29 100644 --- a/cmd/blockchaincmd/upgradecmd/apply.go +++ b/cmd/blockchaincmd/upgradecmd/apply.go @@ -254,6 +254,7 @@ func applyPublicNetworkUpgrade(blockchainName, networkKey string, sc *models.Sid if print { blockchainIDstr := "" if sc.Networks != nil && + !sc.NetworkDataIsEmpty(networkKey) && sc.Networks[networkKey].BlockchainID != ids.Empty { blockchainIDstr = sc.Networks[networkKey].BlockchainID.String() } @@ -304,7 +305,7 @@ func applyPublicNetworkUpgrade(blockchainName, networkKey string, sc *models.Sid ux.Logger.PrintToUser("Trying to install the upgrade files at the provided %s path", avalanchegoChainConfigDir) chainDir := filepath.Join(avalanchegoChainConfigDir, sc.Networks[networkKey].BlockchainID.String()) - destPath := filepath.Join(chainDir, constants.UpgradeBytesFileName) + destPath := filepath.Join(chainDir, constants.UpgradeFileName) if err = os.Mkdir(chainDir, constants.DefaultPerms755); err != nil && !os.IsExist(err) { return fmt.Errorf("failed to create blockchain directory: %w", err) } @@ -318,6 +319,9 @@ func applyPublicNetworkUpgrade(blockchainName, networkKey string, sc *models.Sid func validateUpgrade(blockchainName, networkKey string, sc *models.Sidecar, skipPrompting bool) ([]params.PrecompileUpgrade, string, error) { // if there's no entry in the Sidecar, we assume there hasn't been a deploy yet + if sc.NetworkDataIsEmpty(networkKey) { + return nil, "", subnetNotYetDeployed() + } chainID := sc.Networks[networkKey].BlockchainID if chainID == ids.Empty { return nil, "", errors.New(ErrSubnetNotDeployedOutput) diff --git a/cmd/blockchaincmd/upgradecmd/generate.go b/cmd/blockchaincmd/upgradecmd/generate.go index b4cd36dbd..da8052416 100644 --- a/cmd/blockchaincmd/upgradecmd/generate.go +++ b/cmd/blockchaincmd/upgradecmd/generate.go @@ -598,7 +598,7 @@ func promptAdminManagerAndEnabledAddresses( sc *models.Sidecar, action string, ) ([]common.Address, []common.Address, []common.Address, bool, error) { - allowList, cancelled, err := vm.GenerateAllowList(app, action, sc.VMVersion) + allowList, cancelled, err := vm.GenerateAllowList(app, vm.AllowList{}, action, sc.VMVersion) if cancelled || err != nil { return nil, nil, nil, cancelled, err } diff --git a/cmd/contractcmd/contract.go b/cmd/contractcmd/contract.go index 84df73504..471355d56 100644 --- a/cmd/contractcmd/contract.go +++ b/cmd/contractcmd/contract.go @@ -22,5 +22,7 @@ and interacting with smart contracts.`, app = injectedApp // contract deploy cmd.AddCommand(newDeployCmd()) + // contract initValidatorManager + cmd.AddCommand(newInitValidatorManagerCmd()) return cmd } diff --git a/cmd/contractcmd/init_validator_manager.go b/cmd/contractcmd/init_validator_manager.go new file mode 100644 index 000000000..0f6308a8b --- /dev/null +++ b/cmd/contractcmd/init_validator_manager.go @@ -0,0 +1,220 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package contractcmd + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/networkoptions" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/pkg/validatormanager" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" +) + +type ValidatorManagerFlags struct { + Network networkoptions.NetworkFlags + PrivateKeyFlags contract.PrivateKeyFlags + rpcEndpoint string + aggregatorLogLevel string + aggregatorExtraEndpoints []string +} + +type POSManagerSpecFlags struct { + rewardCalculatorAddress string + minimumStakeAmount uint64 // big.Int + maximumStakeAmount uint64 // big.Int + minimumStakeDuration uint64 + minimumDelegationFee uint16 + maximumStakeMultiplier uint8 + weightToValueFactor uint64 // big.Int +} + +var ( + validatorManagerSupportedNetworkOptions = []networkoptions.NetworkOption{ + networkoptions.Local, + networkoptions.Devnet, + networkoptions.EtnaDevnet, + networkoptions.Fuji, + } + validatorManagerFlags ValidatorManagerFlags + initPOSManagerFlags POSManagerSpecFlags +) + +// avalanche contract initValidatorManager +func newInitValidatorManagerCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "initValidatorManager blockchainName", + Short: "Initializes Proof of Authority(PoA) or Proof of Stake(PoS) Validator Manager on a given Network and Blockchain", + Long: "Initializes Proof of Authority(PoA) or Proof of Stake(PoS)Validator Manager contract on a Blockchain and sets up initial validator set on the Blockchain. For more info on Validator Manager, please head to https://github.com/ava-labs/teleporter/tree/staking-contract/contracts/validator-manager", + RunE: initValidatorManager, + Args: cobrautils.ExactArgs(1), + } + networkoptions.AddNetworkFlagsToCmd(cmd, &validatorManagerFlags.Network, true, validatorManagerSupportedNetworkOptions) + validatorManagerFlags.PrivateKeyFlags.AddToCmd(cmd, "as contract deployer") + cmd.Flags().StringVar(&validatorManagerFlags.rpcEndpoint, "rpc", "", "deploy the contract into the given rpc endpoint") + cmd.Flags().StringSliceVar(&validatorManagerFlags.aggregatorExtraEndpoints, "aggregator-extra-endpoints", nil, "endpoints for extra nodes that are needed in signature aggregation") + cmd.Flags().StringVar(&validatorManagerFlags.aggregatorLogLevel, "aggregator-log-level", "Off", "log level to use with signature aggregator") + + cmd.Flags().StringVar(&initPOSManagerFlags.rewardCalculatorAddress, "pos-reward-calculator-address", "", "(PoS only) initialize the ValidatorManager with reward calculator address") + cmd.Flags().Uint64Var(&initPOSManagerFlags.minimumStakeAmount, "pos-minimum-stake-amount", 1, "(PoS only) minimum stake amount") + cmd.Flags().Uint64Var(&initPOSManagerFlags.maximumStakeAmount, "pos-maximum-stake-amount", 1000, "(PoS only) maximum stake amount") + cmd.Flags().Uint64Var(&initPOSManagerFlags.minimumStakeDuration, "pos-minimum-stake-duration", 100, "(PoS only) minimum stake duration") + cmd.Flags().Uint16Var(&initPOSManagerFlags.minimumDelegationFee, "pos-minimum-delegation-fee", 1, "(PoS only) minimum delegation fee") + cmd.Flags().Uint8Var(&initPOSManagerFlags.maximumStakeMultiplier, "pos-maximum-stake-multiplier", 1, "(PoS only )maximum stake multiplier") + cmd.Flags().Uint64Var(&initPOSManagerFlags.weightToValueFactor, "pos-weight-to-value-factor", 1, "(PoS only) weight to value factor") + return cmd +} + +func initValidatorManager(_ *cobra.Command, args []string) error { + blockchainName := args[0] + chainSpec := contract.ChainSpec{ + BlockchainName: blockchainName, + } + network, err := networkoptions.GetNetworkFromCmdLineFlags( + app, + "", + validatorManagerFlags.Network, + true, + false, + validatorManagerSupportedNetworkOptions, + "", + ) + if err != nil { + return err + } + if network.ClusterName != "" { + network = models.ConvertClusterToNetwork(network) + } + if validatorManagerFlags.rpcEndpoint == "" { + validatorManagerFlags.rpcEndpoint, _, err = contract.GetBlockchainEndpoints( + app, + network, + chainSpec, + true, + false, + ) + if err != nil { + return err + } + } + ux.Logger.PrintToUser(logging.Yellow.Wrap("RPC Endpoint: %s"), validatorManagerFlags.rpcEndpoint) + genesisAddress, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + privateKey, err := validatorManagerFlags.PrivateKeyFlags.GetPrivateKey(app, genesisPrivateKey) + if err != nil { + return err + } + if privateKey == "" { + privateKey, err = prompts.PromptPrivateKey( + app.Prompt, + "pay for initializing Proof of Authority Validator Manager contract? (Uses Blockchain gas token)", + app.GetKeyDir(), + app.GetKey, + genesisAddress, + genesisPrivateKey, + ) + if err != nil { + return err + } + } + sc, err := app.LoadSidecar(chainSpec.BlockchainName) + if err != nil { + return fmt.Errorf("failed to load sidecar: %w", err) + } + scNetwork := sc.Networks[network.Name()] + if scNetwork.BlockchainID == ids.Empty { + return fmt.Errorf("blockchain has not been deployed to %s", network.Name()) + } + bootstrapValidators := scNetwork.BootstrapValidators + avaGoBootstrapValidators, err := blockchaincmd.ConvertToAvalancheGoSubnetValidator(bootstrapValidators) + if err != nil { + return err + } + clusterName := scNetwork.ClusterName + extraAggregatorPeers, err := blockchaincmd.GetAggregatorExtraPeers(clusterName, validatorManagerFlags.aggregatorExtraEndpoints) + if err != nil { + return err + } + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + blockchainID, err := contract.GetBlockchainID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + ownerAddress := common.HexToAddress(sc.ProxyContractOwner) + subnetSDK := blockchainSDK.Subnet{ + SubnetID: subnetID, + BlockchainID: blockchainID, + BootstrapValidators: avaGoBootstrapValidators, + OwnerAddress: &ownerAddress, + RPC: validatorManagerFlags.rpcEndpoint, + } + switch { + case sc.PoA(): // PoA + ux.Logger.PrintToUser(logging.Yellow.Wrap("Initializing Proof of Authority Validator Manager contract on blockchain %s"), blockchainName) + if err := validatormanager.SetupPoA( + subnetSDK, + network, + privateKey, + extraAggregatorPeers, + validatorManagerFlags.aggregatorLogLevel, + ); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("Proof of Authority Validator Manager contract successfully initialized on blockchain %s", blockchainName) + case sc.PoS(): // PoS + ux.Logger.PrintToUser(logging.Yellow.Wrap("Initializing Proof of Stake Validator Manager contract on blockchain %s"), blockchainName) + if initPOSManagerFlags.rewardCalculatorAddress == "" { + initPOSManagerFlags.rewardCalculatorAddress = validatorManagerSDK.RewardCalculatorAddress + } + if err := validatormanager.SetupPoS( + subnetSDK, + network, + privateKey, + extraAggregatorPeers, + validatorManagerFlags.aggregatorLogLevel, + validatorManagerSDK.PoSParams{ + MinimumStakeAmount: big.NewInt(int64(initPOSManagerFlags.minimumStakeAmount)), + MaximumStakeAmount: big.NewInt(int64(initPOSManagerFlags.maximumStakeAmount)), + MinimumStakeDuration: initPOSManagerFlags.minimumStakeDuration, + MinimumDelegationFee: initPOSManagerFlags.minimumDelegationFee, + MaximumStakeMultiplier: initPOSManagerFlags.maximumStakeMultiplier, + WeightToValueFactor: big.NewInt(int64(initPOSManagerFlags.weightToValueFactor)), + RewardCalculatorAddress: initPOSManagerFlags.rewardCalculatorAddress, + }, + ); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("Native Token Proof of Stake Validator Manager contract successfully initialized on blockchain %s", blockchainName) + default: // unsupported + return fmt.Errorf("only PoA and PoS supported") + } + return nil +} diff --git a/cmd/networkcmd/clean.go b/cmd/networkcmd/clean.go index 8b43a10a3..c4266468c 100644 --- a/cmd/networkcmd/clean.go +++ b/cmd/networkcmd/clean.go @@ -51,7 +51,11 @@ func clean(*cobra.Command, []string) error { app.Log.Warn("failed resetting default snapshot", zap.Error(err)) } - if err := binutils.KillgRPCServerProcess(app); err != nil { + if err := binutils.KillgRPCServerProcess( + app, + binutils.LocalNetworkGRPCServerEndpoint, + constants.ServerRunFileLocalNetworkPrefix, + ); err != nil { app.Log.Warn("failed killing server process", zap.Error(err)) } else { ux.Logger.PrintToUser("Process terminated.") diff --git a/cmd/networkcmd/start.go b/cmd/networkcmd/start.go index 17677cc07..681234d2f 100644 --- a/cmd/networkcmd/start.go +++ b/cmd/networkcmd/start.go @@ -3,7 +3,6 @@ package networkcmd import ( - "context" "fmt" "path/filepath" @@ -18,7 +17,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-cli/pkg/vm" "github.com/ava-labs/avalanche-network-runner/client" - "github.com/ava-labs/avalanche-network-runner/server" anrutils "github.com/ava-labs/avalanche-network-runner/utils" "github.com/spf13/cobra" ) @@ -68,7 +66,13 @@ func StartNetwork(*cobra.Command, []string) error { } sd := subnet.NewLocalDeployer(app, avagoVersion, avagoBinaryPath, "", false) - if err := sd.StartServer(); err != nil { + if err := sd.StartServer( + constants.ServerRunFileLocalNetworkPrefix, + binutils.LocalNetworkGRPCServerPort, + binutils.LocalNetworkGRPCGatewayPort, + app.GetSnapshotsDir(), + "", + ); err != nil { return err } @@ -85,7 +89,7 @@ func StartNetwork(*cobra.Command, []string) error { ctx, cancel := utils.GetANRContext() defer cancel() - bootstrapped, err := checkNetworkIsAlreadyBootstrapped(ctx, cli) + bootstrapped, err := localnet.CheckNetworkIsAlreadyBootstrapped(ctx, cli) if err != nil { return err } @@ -242,14 +246,3 @@ func determineAvagoVersion(userProvidedAvagoVersion string) (string, error) { constants.AvalancheGoCompatibilityURL, ) } - -func checkNetworkIsAlreadyBootstrapped(ctx context.Context, cli client.Client) (bool, error) { - _, err := cli.Status(ctx) - if err != nil { - if server.IsServerError(err, server.ErrNotBootstrapped) { - return false, nil - } - return false, fmt.Errorf("failed trying to get network status: %w", err) - } - return true, nil -} diff --git a/cmd/networkcmd/stop.go b/cmd/networkcmd/stop.go index 69e23f33a..ff0180350 100644 --- a/cmd/networkcmd/stop.go +++ b/cmd/networkcmd/stop.go @@ -51,7 +51,11 @@ func StopNetwork(*cobra.Command, []string) error { } var err error - if err = binutils.KillgRPCServerProcess(app); err != nil { + if err = binutils.KillgRPCServerProcess( + app, + binutils.LocalNetworkGRPCServerEndpoint, + constants.ServerRunFileLocalNetworkPrefix, + ); err != nil { app.Log.Warn("failed killing server process", zap.Error(err)) fmt.Println(err) } else { diff --git a/cmd/nodecmd/add_dashboard.go b/cmd/nodecmd/add_dashboard.go index a683e8928..6de90bf64 100644 --- a/cmd/nodecmd/add_dashboard.go +++ b/cmd/nodecmd/add_dashboard.go @@ -28,6 +28,13 @@ cluster.`, func addDashboard(_ *cobra.Command, args []string) error { clusterName := args[0] + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + if clusterConfig.Local { + return notImplementedForLocal("addDashboard") + } if customGrafanaDashboardPath != "" { if err := addCustomDashboard(clusterName, blockchainName); err != nil { return err diff --git a/cmd/nodecmd/create.go b/cmd/nodecmd/create.go index 5f8d66553..bb66d69b5 100644 --- a/cmd/nodecmd/create.go +++ b/cmd/nodecmd/create.go @@ -14,15 +14,15 @@ import ( "sync" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" "github.com/ava-labs/avalanche-cli/pkg/docker" "github.com/ava-labs/avalanche-cli/pkg/metrics" - "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/cmd/flags" "github.com/ava-labs/avalanche-cli/pkg/ansible" - "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -45,7 +45,7 @@ const ( ) var ( - createSupportedNetworkOptions = []networkoptions.NetworkOption{networkoptions.Fuji, networkoptions.Devnet} + createSupportedNetworkOptions = []networkoptions.NetworkOption{networkoptions.Fuji, networkoptions.Devnet, networkoptions.EtnaDevnet} globalNetworkFlags networkoptions.NetworkFlags useAWS bool useGCP bool @@ -70,12 +70,9 @@ var ( iops int volumeType string volumeSize int - versionComments = map[string]string{ - "v1.11.0-fuji": " (recommended for fuji durango)", - } - grafanaPkg string - wizSubnet string - publicHTTPPortAccess bool + grafanaPkg string + wizSubnet string + publicHTTPPortAccess bool ) func newCreateCmd() *cobra.Command { @@ -128,6 +125,11 @@ will apply to all nodes in the cluster`, cmd.Flags().IntVar(&volumeSize, "aws-volume-size", constants.CloudServerStorageSize, "AWS volume size in GB") cmd.Flags().BoolVar(&replaceKeyPair, "auto-replace-keypair", false, "automatically replaces key pair to access node if previous key pair is not found") cmd.Flags().BoolVar(&publicHTTPPortAccess, "public-http-port", false, "allow public access to avalanchego HTTP port") + cmd.Flags().StringArrayVar(&bootstrapIDs, "bootstrap-ids", []string{}, "nodeIDs of bootstrap nodes") + cmd.Flags().StringArrayVar(&bootstrapIPs, "bootstrap-ips", []string{}, "IP:port pairs of bootstrap nodes") + cmd.Flags().StringVar(&genesisPath, "genesis", "", "path to genesis file") + cmd.Flags().StringVar(&upgradePath, "upgrade", "", "path to upgrade file") + cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "primary network partial sync") return cmd } @@ -135,8 +137,15 @@ will apply to all nodes in the cluster`, func handlePostRun(_ *cobra.Command, _ []string) {} func preCreateChecks(clusterName string) error { - if !flags.EnsureMutuallyExclusive([]bool{useLatestAvalanchegoReleaseVersion, useLatestAvalanchegoPreReleaseVersion, useAvalanchegoVersionFromSubnet != "", useCustomAvalanchegoVersion != ""}) { - return fmt.Errorf("latest avalanchego released version, latest avalanchego pre-released version, custom avalanchego version and avalanchego version based on given subnet, are mutually exclusive options") + if useCustomAvalanchegoVersion != "" || useAvalanchegoVersionFromSubnet != "" { + useLatestAvalanchegoReleaseVersion = false + useLatestAvalanchegoPreReleaseVersion = false + } + if !flags.EnsureMutuallyExclusive([]bool{useLatestAvalanchegoReleaseVersion, useLatestAvalanchegoPreReleaseVersion}) { + return fmt.Errorf("latest avalanchego released version, latest avalanchego pre-released version are mutually exclusive options") + } + if !flags.EnsureMutuallyExclusive([]bool{useAvalanchegoVersionFromSubnet != "", useCustomAvalanchegoVersion != ""}) { + return fmt.Errorf("custom avalanchego version and avalanchego version based on given subnet, are mutually exclusive options") } if useAWS && useGCP { return fmt.Errorf("could not use both AWS and GCP cloud options") @@ -199,12 +208,47 @@ func preCreateChecks(clusterName string) error { if err := failForExternal(clusterName); err != nil { return err } + // check for local + clusterConfig := models.ClusterConfig{} + if ok, err := app.ClusterExists(clusterName); err != nil { + return err + } else if ok { + clusterConfig, err = app.GetClusterConfig(clusterName) + if err != nil { + return err + } + } + if clusterConfig.Local { + return notImplementedForLocal("create") + } // bootsrap checks + if globalNetworkFlags.UseEtnaDevnet && (len(bootstrapIDs) != 0 || len(bootstrapIPs) != 0 || genesisPath != "" || upgradePath != "") { + return fmt.Errorf("etna devnet uses predefined bootsrap configuration") + } + if len(bootstrapIDs) != len(bootstrapIPs) { + return fmt.Errorf("number of bootstrap ids and ip:port pairs must be equal") + } + if genesisPath != "" && !utils.FileExists(genesisPath) { + return fmt.Errorf("genesis file %s does not exist", genesisPath) + } + if upgradePath != "" && !utils.FileExists(upgradePath) { + return fmt.Errorf("upgrade file %s does not exist", upgradePath) + } + // check ip:port pairs + for _, ipPortPair := range bootstrapIPs { + if ok := utils.IsValidIPPort(ipPortPair); !ok { + return fmt.Errorf("invalid ip:port pair %s", ipPortPair) + } + } + if globalNetworkFlags.UseDevnet { + partialSync = false + ux.Logger.PrintToUser("disabling partial sync default for devnet") + } return nil } func checkClusterExternal(clusterName string) (bool, error) { - clusterExists, err := checkClusterExists(clusterName) + clusterExists, err := node.CheckClusterExists(app, clusterName) if err != nil { return false, fmt.Errorf("error checking cluster: %w", err) } @@ -241,9 +285,6 @@ func stringToAWSVolumeType(input string) types.VolumeType { func createNodes(cmd *cobra.Command, args []string) error { clusterName := args[0] - if err := preCreateChecks(clusterName); err != nil { - return err - } network, err := networkoptions.GetNetworkFromCmdLineFlags( app, "", @@ -253,12 +294,53 @@ func createNodes(cmd *cobra.Command, args []string) error { createSupportedNetworkOptions, "", ) - if err != nil { + if err := preCreateChecks(clusterName); err != nil { return err } + if network.Kind == models.EtnaDevnet { + publicHTTPPortAccess = true // public http port access for etna devnet api for PoAManagerDeployment + bootstrapIDs = constants.EtnaDevnetBootstrapNodeIDs + bootstrapIPs = constants.EtnaDevnetBootstrapIPs + + // create genesis and upgrade files + genesisTmpFile, err := os.CreateTemp("", "genesis") + if err != nil { + return err + } + if _, err := genesisTmpFile.Write(constants.EtnaDevnetGenesisData); err != nil { + return err + } + if err := genesisTmpFile.Close(); err != nil { + return err + } + genesisPath = genesisTmpFile.Name() + + upgradeTmpFile, err := os.CreateTemp("", "upgrade") + if err != nil { + return err + } + if _, err := upgradeTmpFile.Write(constants.EtnaDevnetUpgradeData); err != nil { + return err + } + if err := upgradeTmpFile.Close(); err != nil { + return err + } + upgradePath = upgradeTmpFile.Name() + + defer func() { + _ = os.Remove(genesisTmpFile.Name()) + _ = os.Remove(upgradeTmpFile.Name()) + }() + } network = models.NewNetworkFromCluster(network, clusterName) globalNetworkFlags.UseDevnet = network.Kind == models.Devnet // set globalNetworkFlags.UseDevnet to true if network is devnet for further use - avalancheGoVersion, err := getAvalancheGoVersion() + avaGoVersionSetting := node.AvalancheGoVersionSettings{ + UseAvalanchegoVersionFromSubnet: useAvalanchegoVersionFromSubnet, + UseLatestAvalanchegoReleaseVersion: useLatestAvalanchegoReleaseVersion, + UseLatestAvalanchegoPreReleaseVersion: useLatestAvalanchegoPreReleaseVersion, + UseCustomAvalanchegoVersion: useCustomAvalanchegoVersion, + } + avalancheGoVersion, err := node.GetAvalancheGoVersion(app, avaGoVersionSetting) if err != nil { return err } @@ -392,7 +474,7 @@ func createNodes(cmd *cobra.Command, args []string) error { } else { if cloudService == constants.AWSCloudService { // Get AWS Credential, region and AMI - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.AWSCloudService) != nil) { + if !(authorizeAccess || node.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) { return fmt.Errorf("cloud access is required") } ec2SvcMap, ami, numNodesMap, err := getAWSCloudConfig(awsProfile, false, nil, nodeType) @@ -464,7 +546,7 @@ func createNodes(cmd *cobra.Command, args []string) error { } } } else { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || node.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return fmt.Errorf("cloud access is required") } // Get GCP Credential, zone, Image ID, service account key file path, and GCP project name @@ -715,7 +797,17 @@ func createNodes(cmd *cobra.Command, args []string) error { spinner = spinSession.SpinToUser(utils.ScriptLog(host.NodeID, "Setup AvalancheGo")) // check if host is a API host publicAccessToHTTPPort := slices.Contains(cloudConfigMap.GetAllAPIInstanceIDs(), host.GetCloudID()) || publicHTTPPortAccess - if err := docker.ComposeSSHSetupNode(host, network, avalancheGoVersion, addMonitoring, publicAccessToHTTPPort); err != nil { + if err := docker.ComposeSSHSetupNode(host, + network, + avalancheGoVersion, + bootstrapIDs, + bootstrapIPs, + partialSync, + genesisPath, + upgradePath, + addMonitoring, + publicAccessToHTTPPort, + ); err != nil { nodeResults.AddResult(host.NodeID, nil, err) ux.SpinFailWithError(spinner, "", err) return @@ -833,28 +925,26 @@ func saveExternalHostConfig(externalHostConfig models.RegionConfig, hostRegion, } func getExistingMonitoringInstance(clusterName string) (string, error) { - if app.ClustersConfigExists() { - clustersConfig, err := app.LoadClustersConfig() + // check for local + clusterConfig := models.ClusterConfig{} + if ok, err := app.ClusterExists(clusterName); err != nil { + return "", err + } else if ok { + clusterConfig, err = app.GetClusterConfig(clusterName) if err != nil { return "", err } - if _, ok := clustersConfig.Clusters[clusterName]; ok { - if clustersConfig.Clusters[clusterName].MonitoringInstance != "" { - return clustersConfig.Clusters[clusterName].MonitoringInstance, nil - } - } + } + if clusterConfig.MonitoringInstance != "" { + return clusterConfig.MonitoringInstance, nil } return "", nil } func updateKeyPairClustersConfig(cloudConfig models.NodeConfig) error { - clustersConfig := models.ClustersConfig{} - var err error - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if clustersConfig.KeyPair == nil { clustersConfig.KeyPair = make(map[string]string) @@ -887,20 +977,16 @@ func getNodeCloudConfig(node string) (models.RegionConfig, string, error) { } func addNodeToClustersConfig(network models.Network, nodeID, clusterName string, isAPIInstance bool, isExternalHost bool, nodeRole, loadTestName string) error { - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - var err error - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if clustersConfig.Clusters == nil { clustersConfig.Clusters = make(map[string]models.ClusterConfig) } clusterConfig := clustersConfig.Clusters[clusterName] // if supplied network in argument is empty, don't change current cluster network in cluster_config.json - if network != models.UndefinedNetwork { + if !network.IsUndefined() { clusterConfig.Network = network } clusterConfig.HTTPAccess = constants.HTTPAccess(publicHTTPPortAccess) @@ -987,57 +1073,6 @@ func provideStakingCertAndKey(host *models.Host) error { return ssh.RunSSHUploadStakingFiles(host, keyPath) } -// getAvalancheGoVersion asks users whether they want to install the newest Avalanche Go version -// or if they want to use the newest Avalanche Go Version that is still compatible with Subnet EVM -// version of their choice -func getAvalancheGoVersion() (string, error) { - // skip this logic if custom-avalanchego-version flag is set - if useCustomAvalanchegoVersion != "" { - return useCustomAvalanchegoVersion, nil - } - latestReleaseVersion, err := app.Downloader.GetLatestReleaseVersion(binutils.GetGithubLatestReleaseURL( - constants.AvaLabsOrg, - constants.AvalancheGoRepoName, - )) - if err != nil { - return "", err - } - latestPreReleaseVersion, err := app.Downloader.GetLatestPreReleaseVersion( - constants.AvaLabsOrg, - constants.AvalancheGoRepoName, - ) - if err != nil { - return "", err - } - - if !useLatestAvalanchegoReleaseVersion && !useLatestAvalanchegoPreReleaseVersion && useCustomAvalanchegoVersion == "" && useAvalanchegoVersionFromSubnet == "" { - err := promptAvalancheGoVersionChoice(latestReleaseVersion, latestPreReleaseVersion) - if err != nil { - return "", err - } - } - - var version string - switch { - case useLatestAvalanchegoReleaseVersion: - version = latestReleaseVersion - case useLatestAvalanchegoPreReleaseVersion: - version = latestPreReleaseVersion - case useCustomAvalanchegoVersion != "": - version = useCustomAvalanchegoVersion - case useAvalanchegoVersionFromSubnet != "": - sc, err := app.LoadSidecar(useAvalanchegoVersionFromSubnet) - if err != nil { - return "", err - } - version, err = GetLatestAvagoVersionForRPC(sc.RPCVersion, latestPreReleaseVersion) - if err != nil { - return "", err - } - } - return version, nil -} - func GetLatestAvagoVersionForRPC(configuredRPCVersion int, latestPreReleaseVersion string) (string, error) { desiredAvagoVersion, err := vm.GetLatestAvalancheGoByProtocolVersion( app, configuredRPCVersion, constants.AvalancheGoCompatibilityURL) @@ -1051,51 +1086,6 @@ func GetLatestAvagoVersionForRPC(configuredRPCVersion int, latestPreReleaseVersi return desiredAvagoVersion, nil } -// promptAvalancheGoVersionChoice sets flags for either using the latest Avalanche Go -// version or using the latest Avalanche Go version that is still compatible with the subnet that user -// wants the cloud server to track -func promptAvalancheGoVersionChoice(latestReleaseVersion string, latestPreReleaseVersion string) error { - latestReleaseVersionOption := "Use latest Avalanche Go Release Version" + versionComments[latestReleaseVersion] - latestPreReleaseVersionOption := "Use latest Avalanche Go Pre-release Version" + versionComments[latestPreReleaseVersion] - subnetBasedVersionOption := "Use the deployed Subnet's VM version that the node will be validating" - customOption := "Custom" - - txt := "What version of Avalanche Go would you like to install in the node?" - versionOptions := []string{latestReleaseVersionOption, subnetBasedVersionOption, customOption} - if latestPreReleaseVersion != latestReleaseVersion { - versionOptions = []string{latestPreReleaseVersionOption, latestReleaseVersionOption, subnetBasedVersionOption, customOption} - } - versionOption, err := app.Prompt.CaptureList(txt, versionOptions) - if err != nil { - return err - } - - switch versionOption { - case latestReleaseVersionOption: - useLatestAvalanchegoReleaseVersion = true - case latestPreReleaseVersionOption: - useLatestAvalanchegoPreReleaseVersion = true - case customOption: - useCustomAvalanchegoVersion, err = app.Prompt.CaptureVersion("Which version of AvalancheGo would you like to install? (Use format v1.10.13)") - if err != nil { - return err - } - default: - for { - useAvalanchegoVersionFromSubnet, err = app.Prompt.CaptureString("Which Subnet would you like to use to choose the avalanche go version?") - if err != nil { - return err - } - _, err = blockchaincmd.ValidateSubnetNameAndGetChains([]string{useAvalanchegoVersionFromSubnet}) - if err == nil { - break - } - ux.Logger.PrintToUser(fmt.Sprintf("no subnet named %s found", useAvalanchegoVersionFromSubnet)) - } - } - return nil -} - func setCloudService() (string, error) { if utils.IsE2E() { if !utils.E2EDocker() { diff --git a/cmd/nodecmd/create_devnet.go b/cmd/nodecmd/create_devnet.go index 889d0eb40..08caae6bb 100644 --- a/cmd/nodecmd/create_devnet.go +++ b/cmd/nodecmd/create_devnet.go @@ -3,6 +3,7 @@ package nodecmd import ( + _ "embed" "encoding/json" "fmt" "os" @@ -12,6 +13,8 @@ import ( "sync" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + "golang.org/x/exp/slices" "github.com/ava-labs/avalanche-cli/pkg/ansible" @@ -41,6 +44,9 @@ const ( allocationCommonEthAddress = "0xb3d82b1367d362de99ab59a658165aff520cbd4d" ) +//go:embed upgrade.json +var upgradeBytes []byte + func generateCustomCchainGenesis() ([]byte, error) { cChainGenesisMap := map[string]interface{}{} cChainGenesisMap["config"] = coreth_params.GetChainConfig(avago_upgrade.GetConfig(avago_constants.LocalID), coreth_params.AvalancheLocalChainID) @@ -151,7 +157,7 @@ func generateCustomGenesis( } func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[string]string) error { - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } inventoryPath := app.GetAnsibleInventoryDirPath(clusterName) @@ -174,7 +180,7 @@ func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[stri } else { endpointIP = ansibleHosts[ansibleHostIDs[0]].IP } - endpoint := getAvalancheGoEndpoint(endpointIP) + endpoint := node.GetAvalancheGoEndpoint(endpointIP) network := models.NewDevnetNetwork(endpoint, 0) network = models.NewNetworkFromCluster(network, clusterName) @@ -222,15 +228,20 @@ func setupDevnet(clusterName string, hosts []*models.Host, apiNodeIPMap map[stri confMap[config.NetworkNameKey] = fmt.Sprintf("network-%d", network.ID) confMap[config.BootstrapIDsKey] = strings.Join(bootstrapIDs, ",") confMap[config.BootstrapIPsKey] = strings.Join(bootstrapIPs, ",") - confMap[config.GenesisFileKey] = filepath.Join(constants.DockerNodeConfigPath, "genesis.json") + confMap[config.GenesisFileKey] = filepath.Join(constants.DockerNodeConfigPath, constants.GenesisFileName) + confMap[config.UpgradeFileKey] = filepath.Join(constants.DockerNodeConfigPath, constants.UpgradeFileName) + confMap[config.ProposerVMUseCurrentHeightKey] = constants.DevnetFlagsProposerVMUseCurrentHeight confBytes, err := json.MarshalIndent(confMap, "", " ") if err != nil { return err } - if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), "genesis.json"), genesisBytes, constants.WriteReadReadPerms); err != nil { + if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), constants.GenesisFileName), genesisBytes, constants.WriteReadReadPerms); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), constants.UpgradeFileName), upgradeBytes, constants.WriteReadReadPerms); err != nil { return err } - if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), "node.json"), confBytes, constants.WriteReadReadPerms); err != nil { + if err := os.WriteFile(filepath.Join(app.GetNodeInstanceDirPath(host.GetCloudID()), constants.NodeFileName), confBytes, constants.WriteReadReadPerms); err != nil { return err } if slices.Contains(hostsWithoutAPIIDs, host.NodeID) { diff --git a/cmd/nodecmd/create_gcp.go b/cmd/nodecmd/create_gcp.go index 9bbad62bf..6db5241b3 100644 --- a/cmd/nodecmd/create_gcp.go +++ b/cmd/nodecmd/create_gcp.go @@ -53,16 +53,13 @@ func getGCPCloudCredentials() (*compute.Service, string, string, error) { var err error var gcpCredentialsPath string var gcpProjectName string - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return nil, "", "", err - } - if clustersConfig.GCPConfig != (models.GCPConfig{}) { - gcpProjectName = clustersConfig.GCPConfig.ProjectName - gcpCredentialsPath = clustersConfig.GCPConfig.ServiceAccFilePath - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return nil, "", "", err + } + if clustersConfig.GCPConfig != (models.GCPConfig{}) { + gcpProjectName = clustersConfig.GCPConfig.ProjectName + gcpCredentialsPath = clustersConfig.GCPConfig.ServiceAccFilePath } if gcpProjectName == "" { if cmdLineGCPProjectName != "" { @@ -380,13 +377,9 @@ func createGCPInstance( } func updateClustersConfigGCPKeyFilepath(projectName, serviceAccountKeyFilepath string) error { - clustersConfig := models.ClustersConfig{} - var err error - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if projectName != "" { clustersConfig.GCPConfig.ProjectName = projectName diff --git a/cmd/nodecmd/deploy.go b/cmd/nodecmd/deploy.go index 1ba839e58..497d25ac2 100644 --- a/cmd/nodecmd/deploy.go +++ b/cmd/nodecmd/deploy.go @@ -5,6 +5,8 @@ package nodecmd import ( "fmt" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -41,29 +43,32 @@ It saves the deploy info both locally and remotely. func deploySubnet(cmd *cobra.Command, args []string) error { clusterName := args[0] subnetName := args[1] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { return err } - clustersConfig, err := app.LoadClustersConfig() + clusterConfig, err := app.GetClusterConfig(clusterName) if err != nil { return err } - if clustersConfig.Clusters[clusterName].Network.Kind != models.Devnet { + if clusterConfig.Local { + return notImplementedForLocal("deploy") + } + if clusterConfig.Network.Kind != models.Devnet { return fmt.Errorf("node deploy command must be applied to devnet clusters") } hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) if err != nil { return err } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) if !avoidChecks { - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } - if err := checkHostsAreRPCCompatible(hosts, subnetName); err != nil { + if err := node.CheckHostsAreRPCCompatible(app, hosts, subnetName); err != nil { return err } } diff --git a/cmd/nodecmd/destroy.go b/cmd/nodecmd/destroy.go index 9153dc0f3..154db3a2f 100644 --- a/cmd/nodecmd/destroy.go +++ b/cmd/nodecmd/destroy.go @@ -8,11 +8,12 @@ import ( "os" "strings" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" - "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "golang.org/x/exp/maps" @@ -49,13 +50,9 @@ If there is a static IP address attached, it will be released.`, } func removeNodeFromClustersConfig(clusterName string) error { - clustersConfig := models.ClustersConfig{} - var err error - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if clustersConfig.Clusters != nil { delete(clustersConfig.Clusters, clusterName) @@ -124,13 +121,9 @@ func getFirstAvailableNode(nodesToStop []string) (string, bool) { } func Cleanup() error { - var err error - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } clusterNames := maps.Keys(clustersConfig.Clusters) for _, clusterName := range clusterNames { @@ -155,9 +148,16 @@ func destroyNodes(_ *cobra.Command, args []string) error { return Cleanup() } clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := nodePkg.CheckCluster(app, clusterName); err != nil { + return err + } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { return err } + if clusterConfig.Local { + return notImplementedForLocal("destroy") + } isExternalCluster, err := checkClusterExternal(clusterName) if err != nil { return err @@ -169,7 +169,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { if err := getDeleteConfigConfirmation(); err != nil { return err } - nodesToStop, err := getClusterNodes(clusterName) + nodesToStop, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -236,7 +236,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { continue } if nodeConfig.CloudService == "" || nodeConfig.CloudService == constants.AWSCloudService { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.AWSCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) { return fmt.Errorf("cloud access is required") } if err = ec2SvcMap[nodeConfig.Region].DestroyAWSNode(nodeConfig, clusterName); err != nil { @@ -258,7 +258,7 @@ func destroyNodes(_ *cobra.Command, args []string) error { } } } else { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return fmt.Errorf("cloud access is required") } if gcpCloud == nil { @@ -315,49 +315,12 @@ func destroyNodes(_ *cobra.Command, args []string) error { } func getClusterMonitoringNode(clusterName string) (string, error) { - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - var err error - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return "", err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return "", err } if _, ok := clustersConfig.Clusters[clusterName]; !ok { return "", fmt.Errorf("cluster %q does not exist", clusterName) } return clustersConfig.Clusters[clusterName].MonitoringInstance, nil } - -func checkCluster(clusterName string) error { - _, err := getClusterNodes(clusterName) - return err -} - -func checkClusterExists(clusterName string) (bool, error) { - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - var err error - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return false, err - } - } - _, ok := clustersConfig.Clusters[clusterName] - return ok, nil -} - -func getClusterNodes(clusterName string) ([]string, error) { - if exists, err := checkClusterExists(clusterName); err != nil || !exists { - return nil, fmt.Errorf("cluster %q not found", clusterName) - } - clustersConfig, err := app.LoadClustersConfig() - if err != nil { - return nil, err - } - clusterNodes := clustersConfig.Clusters[clusterName].Nodes - if len(clusterNodes) == 0 { - return nil, fmt.Errorf("no nodes found in cluster %s", clusterName) - } - return clusterNodes, nil -} diff --git a/cmd/nodecmd/dynamic_ips.go b/cmd/nodecmd/dynamic_ips.go index 0916d2560..4f5599efe 100644 --- a/cmd/nodecmd/dynamic_ips.go +++ b/cmd/nodecmd/dynamic_ips.go @@ -6,6 +6,8 @@ import ( "context" "fmt" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" @@ -51,7 +53,7 @@ func getPublicIPsForNodesWithDynamicIP(nodesWithDynamicIP []models.NodeConfig) ( } var publicIP map[string]string if node.CloudService == constants.GCPCloudService { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return nil, fmt.Errorf("cloud access is required") } if gcpCloud == nil { @@ -87,7 +89,7 @@ func getPublicIPsForNodesWithDynamicIP(nodesWithDynamicIP []models.NodeConfig) ( // - in ansible inventory file // - in host config file func updatePublicIPs(clusterName string) error { - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } diff --git a/cmd/nodecmd/export.go b/cmd/nodecmd/export.go index 02baa4dba..d25b27e12 100644 --- a/cmd/nodecmd/export.go +++ b/cmd/nodecmd/export.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -51,7 +53,7 @@ func exportFile(_ *cobra.Command, args []string) error { ux.Logger.RedXToUser("file already exists, use --force to overwrite") return nil } - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { ux.Logger.RedXToUser("cluster not found: %v", err) return err } diff --git a/cmd/nodecmd/helpers.go b/cmd/nodecmd/helpers.go index dc3c683f4..5df43d2c2 100644 --- a/cmd/nodecmd/helpers.go +++ b/cmd/nodecmd/helpers.go @@ -2,20 +2,6 @@ // See the file LICENSE for licensing terms. package nodecmd -import ( - "encoding/json" - "errors" - "fmt" - "sync" - - "github.com/ava-labs/avalanche-cli/pkg/constants" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/ssh" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/api/info" -) - // NumNodes is a struct to hold number of nodes with and without stake type NumNodes struct { numValidators int // with stake @@ -25,237 +11,3 @@ type NumNodes struct { func (n NumNodes) All() int { return n.numValidators + n.numAPI } - -func getUnhealthyNodes(hosts []*models.Host) ([]string, error) { - wg := sync.WaitGroup{} - wgResults := models.NodeResults{} - for _, host := range hosts { - wg.Add(1) - go func(nodeResults *models.NodeResults, host *models.Host) { - defer wg.Done() - if resp, err := ssh.RunSSHCheckHealthy(host); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - return - } else { - if isHealthy, err := parseHealthyOutput(resp); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - } else { - nodeResults.AddResult(host.GetCloudID(), isHealthy, err) - } - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return nil, fmt.Errorf("failed to get health status for node(s) %s", wgResults.GetErrorHostMap()) - } - return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool { - return !wgResults.GetResultMap()[nodeID].(bool) - }), nil -} - -func parseHealthyOutput(byteValue []byte) (bool, error) { - var result map[string]interface{} - if err := json.Unmarshal(byteValue, &result); err != nil { - return false, err - } - isHealthyInterface, ok := result["result"].(map[string]interface{}) - if ok { - isHealthy, ok := isHealthyInterface["healthy"].(bool) - if ok { - return isHealthy, nil - } - } - return false, fmt.Errorf("unable to parse node healthy status") -} - -func getNotBootstrappedNodes(hosts []*models.Host) ([]string, error) { - wg := sync.WaitGroup{} - wgResults := models.NodeResults{} - for _, host := range hosts { - wg.Add(1) - go func(nodeResults *models.NodeResults, host *models.Host) { - defer wg.Done() - if resp, err := ssh.RunSSHCheckBootstrapped(host); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - return - } else { - if isBootstrapped, err := parseBootstrappedOutput(resp); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - } else { - nodeResults.AddResult(host.GetCloudID(), isBootstrapped, err) - } - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return nil, fmt.Errorf("failed to get avalanchego bootstrap status for node(s) %s", wgResults.GetErrorHostMap()) - } - return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool { - return !wgResults.GetResultMap()[nodeID].(bool) - }), nil -} - -func parseBootstrappedOutput(byteValue []byte) (bool, error) { - var result map[string]interface{} - if err := json.Unmarshal(byteValue, &result); err != nil { - return false, err - } - isBootstrappedInterface, ok := result["result"].(map[string]interface{}) - if ok { - isBootstrapped, ok := isBootstrappedInterface["isBootstrapped"].(bool) - if ok { - return isBootstrapped, nil - } - } - return false, errors.New("unable to parse node bootstrap status") -} - -func getRPCIncompatibleNodes(hosts []*models.Host, subnetName string) ([]string, error) { - ux.Logger.PrintToUser("Checking compatibility of node(s) avalanche go RPC protocol version with Subnet EVM RPC of subnet %s ...", subnetName) - sc, err := app.LoadSidecar(subnetName) - if err != nil { - return nil, err - } - wg := sync.WaitGroup{} - wgResults := models.NodeResults{} - for _, host := range hosts { - wg.Add(1) - go func(nodeResults *models.NodeResults, host *models.Host) { - defer wg.Done() - if resp, err := ssh.RunSSHCheckAvalancheGoVersion(host); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - return - } else { - if _, rpcVersion, err := parseAvalancheGoOutput(resp); err != nil { - nodeResults.AddResult(host.GetCloudID(), nil, err) - } else { - nodeResults.AddResult(host.GetCloudID(), rpcVersion, err) - } - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return nil, fmt.Errorf("failed to get rpc protocol version for node(s) %s", wgResults.GetErrorHostMap()) - } - incompatibleNodes := []string{} - for nodeID, rpcVersionI := range wgResults.GetResultMap() { - rpcVersion := rpcVersionI.(uint32) - if rpcVersion != uint32(sc.RPCVersion) { - incompatibleNodes = append(incompatibleNodes, nodeID) - } - } - if len(incompatibleNodes) > 0 { - ux.Logger.PrintToUser(fmt.Sprintf("Compatible Avalanche Go RPC version is %d", sc.RPCVersion)) - } - return incompatibleNodes, nil -} - -func parseAvalancheGoOutput(byteValue []byte) (string, uint32, error) { - reply := map[string]interface{}{} - if err := json.Unmarshal(byteValue, &reply); err != nil { - return "", 0, err - } - resultMap := reply["result"] - resultJSON, err := json.Marshal(resultMap) - if err != nil { - return "", 0, err - } - - nodeVersionReply := info.GetNodeVersionReply{} - if err := json.Unmarshal(resultJSON, &nodeVersionReply); err != nil { - return "", 0, err - } - return nodeVersionReply.VMVersions["platform"], uint32(nodeVersionReply.RPCProtocolVersion), nil -} - -func disconnectHosts(hosts []*models.Host) { - for _, host := range hosts { - _ = host.Disconnect() - } -} - -func authorizedAccessFromSettings() bool { - return app.Conf.GetConfigBoolValue(constants.ConfigAuthorizeCloudAccessKey) -} - -func checkHostsAreRPCCompatible(hosts []*models.Host, subnetName string) error { - incompatibleNodes, err := getRPCIncompatibleNodes(hosts, subnetName) - if err != nil { - return err - } - if len(incompatibleNodes) > 0 { - sc, err := app.LoadSidecar(subnetName) - if err != nil { - return err - } - ux.Logger.PrintToUser("Either modify your Avalanche Go version or modify your VM version") - ux.Logger.PrintToUser("To modify your Avalanche Go version: https://docs.avax.network/nodes/maintain/upgrade-your-avalanchego-node") - switch sc.VM { - case models.SubnetEvm: - ux.Logger.PrintToUser("To modify your Subnet-EVM version: https://docs.avax.network/build/subnet/upgrade/upgrade-subnet-vm") - case models.CustomVM: - ux.Logger.PrintToUser("To modify your Custom VM binary: avalanche subnet upgrade vm %s --config", subnetName) - } - ux.Logger.PrintToUser("Yoy can use \"avalanche node upgrade\" to upgrade Avalanche Go and/or Subnet-EVM to their latest versions") - return fmt.Errorf("the Avalanche Go version of node(s) %s is incompatible with VM RPC version of %s", incompatibleNodes, subnetName) - } - return nil -} - -func checkHostsAreHealthy(hosts []*models.Host) error { - ux.Logger.PrintToUser("Checking if node(s) are healthy...") - unhealthyNodes, err := getUnhealthyNodes(hosts) - if err != nil { - return err - } - if len(unhealthyNodes) > 0 { - return fmt.Errorf("node(s) %s are not healthy, please check the issue and try again later", unhealthyNodes) - } - return nil -} - -func checkHostsAreBootstrapped(hosts []*models.Host) error { - notBootstrappedNodes, err := getNotBootstrappedNodes(hosts) - if err != nil { - return err - } - if len(notBootstrappedNodes) > 0 { - return fmt.Errorf("node(s) %s are not bootstrapped yet, please try again later", notBootstrappedNodes) - } - return nil -} - -func getAvalancheGoEndpoint(ip string) string { - return fmt.Sprintf("http://%s:%d", ip, constants.AvalanchegoAPIPort) -} - -func getRPCEndpoint(endpoint string, blockchainID string) string { - return models.NewDevnetNetwork(endpoint, 0).BlockchainEndpoint(blockchainID) -} - -func getWSEndpoint(endpoint string, blockchainID string) string { - return models.NewDevnetNetwork(endpoint, 0).BlockchainWSEndpoint(blockchainID) -} - -func getPublicEndpoints(clusterName string) ([]string, error) { - endpoints := []string{} - clusterConfig, err := app.GetClusterConfig(clusterName) - if err != nil { - return nil, err - } - publicNodes := clusterConfig.APINodes - if clusterConfig.Network.Kind == models.Devnet { - publicNodes = clusterConfig.Nodes - } - for _, cloudID := range publicNodes { - nodeConfig, err := app.LoadClusterNodeConfig(cloudID) - if err != nil { - return nil, err - } - endpoints = append(endpoints, getAvalancheGoEndpoint(nodeConfig.ElasticIP)) - } - return endpoints, nil -} diff --git a/cmd/nodecmd/import.go b/cmd/nodecmd/import.go index e3c397653..f9bd0a018 100644 --- a/cmd/nodecmd/import.go +++ b/cmd/nodecmd/import.go @@ -9,6 +9,8 @@ import ( "os" "path/filepath" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -42,7 +44,7 @@ affecting cloud nodes like node create or node destroy will be not applicable to func importFile(_ *cobra.Command, args []string) error { clusterName := args[0] - if clusterExists, err := checkClusterExists(clusterName); clusterExists || err != nil { + if clusterExists, err := node.CheckClusterExists(app, clusterName); clusterExists || err != nil { ux.Logger.RedXToUser("cluster %s already exists, please use a different name", clusterName) return nil } @@ -109,13 +111,12 @@ func importFile(_ *cobra.Command, args []string) error { // add cluster clustersConfig := models.ClustersConfig{} clustersConfig.Clusters = make(map[string]models.ClusterConfig) - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - ux.Logger.RedXToUser("error loading clusters config: %v", err) - return err - } + clustersConfig, err = app.GetClustersConfig() + if err != nil { + ux.Logger.RedXToUser("error loading clusters config: %v", err) + return err } + importCluster.ClusterConfig.Network.ClusterName = clusterName clustersConfig.Clusters[clusterName] = importCluster.ClusterConfig if err := app.WriteClustersConfigFile(&clustersConfig); err != nil { diff --git a/cmd/nodecmd/list.go b/cmd/nodecmd/list.go index 16588681d..364bbfd75 100644 --- a/cmd/nodecmd/list.go +++ b/cmd/nodecmd/list.go @@ -6,8 +6,9 @@ import ( "sort" "strings" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" - "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/spf13/cobra" @@ -29,13 +30,9 @@ The node list command lists all clusters together with their nodes.`, } func list(_ *cobra.Command, _ []string) error { - var err error - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if len(clustersConfig.Clusters) == 0 { ux.Logger.PrintToUser("There are no clusters defined.") @@ -44,11 +41,12 @@ func list(_ *cobra.Command, _ []string) error { sort.Strings(clusterNames) for _, clusterName := range clusterNames { clusterConf := clustersConfig.Clusters[clusterName] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } + cloudIDs := clusterConf.GetCloudIDs() nodeIDs := []string{} - for _, cloudID := range clusterConf.GetCloudIDs() { + for _, cloudID := range cloudIDs { nodeIDStr := "----------------------------------------" if clusterConf.IsAvalancheGoHost(cloudID) { if nodeID, err := getNodeID(app.GetNodeInstanceDirPath(cloudID)); err != nil { @@ -59,9 +57,12 @@ func list(_ *cobra.Command, _ []string) error { } nodeIDs = append(nodeIDs, nodeIDStr) } - if clusterConf.External { + switch { + case clusterConf.External: ux.Logger.PrintToUser("cluster %q (%s) EXTERNAL", clusterName, clusterConf.Network.Kind.String()) - } else { + case clusterConf.Local: + ux.Logger.PrintToUser("cluster %q (%s) LOCAL", clusterName, clusterConf.Network.Kind.String()) + default: ux.Logger.PrintToUser("Cluster %q (%s)", clusterName, clusterConf.Network.Kind.String()) } for i, cloudID := range clusterConf.GetCloudIDs() { diff --git a/cmd/nodecmd/load_test_start.go b/cmd/nodecmd/load_test_start.go index dea2296d1..cbcb1f60d 100644 --- a/cmd/nodecmd/load_test_start.go +++ b/cmd/nodecmd/load_test_start.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/docker" "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/node" "github.com/ava-labs/avalanche-cli/pkg/prompts" "github.com/ava-labs/avalanche-cli/pkg/ssh" "github.com/ava-labs/avalanche-cli/pkg/utils" @@ -83,7 +84,7 @@ The command will then run the load test binary based on the provided load test r } func preLoadTestChecks(clusterName string) error { - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if useAWS && useGCP { @@ -98,7 +99,7 @@ func preLoadTestChecks(clusterName string) error { if useSSHAgent && !utils.IsSSHAgentAvailable() { return fmt.Errorf("ssh agent is not available") } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := node.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -134,7 +135,7 @@ func startLoadTest(_ *cobra.Command, args []string) error { return err } } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := node.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -409,7 +410,7 @@ func createClusterYAMLFile(clusterName, subnetID, chainID string, separateHost * if err != nil { return err } - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } var apiNodes []nodeInfo @@ -522,15 +523,13 @@ func GetLoadTestScript(app *application.Avalanche) error { } func getExistingLoadTestInstance(clusterName, loadTestName string) (string, error) { - if app.ClustersConfigExists() { - clustersConfig, err := app.LoadClustersConfig() - if err != nil { - return "", err - } - if _, ok := clustersConfig.Clusters[clusterName]; ok { - if _, loadTestExists := clustersConfig.Clusters[clusterName].LoadTestInstance[loadTestName]; loadTestExists { - return clustersConfig.Clusters[clusterName].LoadTestInstance[loadTestName], nil - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return "", err + } + if _, ok := clustersConfig.Clusters[clusterName]; ok { + if _, loadTestExists := clustersConfig.Clusters[clusterName].LoadTestInstance[loadTestName]; loadTestExists { + return clustersConfig.Clusters[clusterName].LoadTestInstance[loadTestName], nil } } return "", nil diff --git a/cmd/nodecmd/load_test_stop.go b/cmd/nodecmd/load_test_stop.go index 8c5f42fbb..4a68360af 100644 --- a/cmd/nodecmd/load_test_stop.go +++ b/cmd/nodecmd/load_test_stop.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" @@ -40,13 +42,9 @@ separate cloud server created to host the load test.`, } func getLoadTestInstancesInCluster(clusterName string) ([]string, error) { - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - var err error - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return nil, err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return nil, err } if _, ok := clustersConfig.Clusters[clusterName]; !ok { return nil, fmt.Errorf("cluster %s doesn't exist", clusterName) @@ -58,13 +56,9 @@ func getLoadTestInstancesInCluster(clusterName string) ([]string, error) { } func checkLoadTestExists(clusterName, loadTestName string) (bool, error) { - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - var err error - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return false, err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return false, err } if _, ok := clustersConfig.Clusters[clusterName]; !ok { return false, fmt.Errorf("cluster %s doesn't exist", clusterName) @@ -102,7 +96,7 @@ func stopLoadTest(_ *cobra.Command, args []string) error { if err != nil { return err } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -214,7 +208,7 @@ func destroyNode(node, clusterName, loadTestName string, ec2Svc *awsAPI.AwsCloud return err } if nodeConfig.CloudService == "" || nodeConfig.CloudService == constants.AWSCloudService { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.AWSCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.AWSCloudService) != nil) { return fmt.Errorf("cloud access is required") } if err = ec2Svc.DestroyAWSNode(nodeConfig, ""); err != nil { @@ -229,7 +223,7 @@ func destroyNode(node, clusterName, loadTestName string, ec2Svc *awsAPI.AwsCloud ux.Logger.PrintToUser("node %s is already destroyed", nodeConfig.NodeID) } } else { - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(constants.GCPCloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(constants.GCPCloudService) != nil) { return fmt.Errorf("cloud access is required") } if err = gcpClient.DestroyGCPNode(nodeConfig, ""); err != nil { @@ -252,13 +246,9 @@ func destroyNode(node, clusterName, loadTestName string, ec2Svc *awsAPI.AwsCloud } func removeLoadTestNodeFromClustersConfig(clusterName, loadTestName string) error { - clustersConfig := models.ClustersConfig{} - var err error - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if clustersConfig.Clusters != nil { if _, ok := clustersConfig.Clusters[clusterName]; !ok { diff --git a/cmd/nodecmd/local.go b/cmd/nodecmd/local.go new file mode 100644 index 000000000..1d83b8b85 --- /dev/null +++ b/cmd/nodecmd/local.go @@ -0,0 +1,237 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package nodecmd + +import ( + "fmt" + "path/filepath" + + "github.com/ava-labs/avalanche-cli/pkg/binutils" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/networkoptions" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/spf13/cobra" +) + +var ( + avalanchegoBinaryPath string + + bootstrapIDs []string + bootstrapIPs []string + genesisPath string + upgradePath string + stakingTLSKeyPath string + stakingCertKeyPath string + stakingSignerKeyPath string + numNodes uint32 + nodeConfigPath string + partialSync bool +) + +// const snapshotName = "local_snapshot" +func newLocalCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "local", + Short: "(ALPHA Warning) Suite of commands for a local avalanche node", + Long: `(ALPHA Warning) This command is currently in experimental mode. + +The node local command suite provides a collection of commands related to local nodes`, + RunE: cobrautils.CommandSuiteUsage, + } + // node local start + cmd.AddCommand(newLocalStartCmd()) + // node local stop + cmd.AddCommand(newLocalStopCmd()) + // node local destroy + cmd.AddCommand(newLocalDestroyCmd()) + // node local track + cmd.AddCommand(newLocalTrackCmd()) + // node local status + cmd.AddCommand(newLocalStatusCmd()) + return cmd +} + +func newLocalStartCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "start [clusterName]", + Short: "(ALPHA Warning) Create a new validator on local machine", + Long: `(ALPHA Warning) This command is currently in experimental mode. + +The node local start command sets up a validator on a local server. +The validator will be validating the Avalanche Primary Network and Subnet +of your choice. By default, the command runs an interactive wizard. It +walks you through all the steps you need to set up a validator. +Once this command is completed, you will have to wait for the validator +to finish bootstrapping on the primary network before running further +commands on it, e.g. validating a Subnet. You can check the bootstrapping +status by running avalanche node status local +`, + Args: cobra.ExactArgs(1), + RunE: localStartNode, + PersistentPostRun: handlePostRun, + } + networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, createSupportedNetworkOptions) + cmd.Flags().BoolVar(&useLatestAvalanchegoReleaseVersion, "latest-avalanchego-version", false, "install latest avalanchego release version on node/s") + cmd.Flags().BoolVar(&useLatestAvalanchegoPreReleaseVersion, "latest-avalanchego-pre-release-version", true, "install latest avalanchego pre-release version on node/s") + cmd.Flags().StringVar(&useCustomAvalanchegoVersion, "custom-avalanchego-version", "", "install given avalanchego version on node/s") + cmd.Flags().StringVar(&avalanchegoBinaryPath, "avalanchego-path", "", "use this avalanchego binary path") + cmd.Flags().StringArrayVar(&bootstrapIDs, "bootstrap-id", []string{}, "nodeIDs of bootstrap nodes") + cmd.Flags().StringArrayVar(&bootstrapIPs, "bootstrap-ip", []string{}, "IP:port pairs of bootstrap nodes") + cmd.Flags().StringVar(&genesisPath, "genesis", "", "path to genesis file") + cmd.Flags().StringVar(&upgradePath, "upgrade", "", "path to upgrade file") + cmd.Flags().StringVar(&stakingTLSKeyPath, "staking-tls-key-path", "", "path to provided staking tls key for node") + cmd.Flags().StringVar(&stakingCertKeyPath, "staking-cert-key-path", "", "path to provided staking cert key for node") + cmd.Flags().StringVar(&stakingSignerKeyPath, "staking-signer-key-path", "", "path to provided staking signer key for node") + cmd.Flags().Uint32Var(&numNodes, "num-nodes", 1, "number of nodes to start") + cmd.Flags().StringVar(&nodeConfigPath, "node-config", "", "path to common avalanchego config settings for all nodes") + cmd.Flags().BoolVar(&partialSync, "partial-sync", true, "primary network partial sync") + return cmd +} + +func newLocalStopCmd() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "(ALPHA Warning) Stop local node", + Long: `Stop local node.`, + Args: cobra.ExactArgs(0), + RunE: localStopNode, + } +} + +func newLocalTrackCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "track [clusterName] [blockchainName]", + Short: "(ALPHA Warning) make the local node at the cluster to track given blockchain", + Long: "(ALPHA Warning) make the local node at the cluster to track given blockchain", + Args: cobra.ExactArgs(2), + RunE: localTrack, + } + cmd.Flags().StringVar(&avalanchegoBinaryPath, "avalanchego-path", "", "use this avalanchego binary path") + cmd.Flags().BoolVar(&useLatestAvalanchegoReleaseVersion, "latest-avalanchego-version", false, "install latest avalanchego release version on node/s") + cmd.Flags().BoolVar(&useLatestAvalanchegoPreReleaseVersion, "latest-avalanchego-pre-release-version", true, "install latest avalanchego pre-release version on node/s") + cmd.Flags().StringVar(&useCustomAvalanchegoVersion, "custom-avalanchego-version", "", "install given avalanchego version on node/s") + return cmd +} + +func newLocalDestroyCmd() *cobra.Command { + return &cobra.Command{ + Use: "destroy [clusterName]", + Short: "(ALPHA Warning) Cleanup local node", + Long: `Cleanup local node.`, + Args: cobra.ExactArgs(1), + RunE: localDestroyNode, + } +} + +func newLocalStatusCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "(ALPHA Warning) Get status of local node", + Long: `Get status of local node.`, + Args: cobra.MaximumNArgs(1), + RunE: localStatus, + } + + cmd.Flags().StringVar(&blockchainName, "subnet", "", "specify the blockchain the node is syncing with") + cmd.Flags().StringVar(&blockchainName, "blockchain", "", "specify the blockchain the node is syncing with") + + return cmd +} + +func localStartNode(_ *cobra.Command, args []string) error { + clusterName := args[0] + anrSettings := node.ANRSettings{ + GenesisPath: genesisPath, + UpgradePath: upgradePath, + BootstrapIDs: bootstrapIDs, + BootstrapIPs: bootstrapIPs, + StakingSignerKeyPath: stakingTLSKeyPath, + StakingCertKeyPath: stakingCertKeyPath, + StakingTLSKeyPath: stakingTLSKeyPath, + } + if useCustomAvalanchegoVersion != "" { + useLatestAvalanchegoReleaseVersion = false + useLatestAvalanchegoPreReleaseVersion = false + } + avaGoVersionSetting := node.AvalancheGoVersionSettings{ + UseCustomAvalanchegoVersion: useCustomAvalanchegoVersion, + UseLatestAvalanchegoPreReleaseVersion: useLatestAvalanchegoPreReleaseVersion, + UseLatestAvalanchegoReleaseVersion: useLatestAvalanchegoReleaseVersion, + } + var ( + err error + nodeConfig map[string]interface{} + ) + if nodeConfigPath != "" { + nodeConfig, err = utils.ReadJSON(nodeConfigPath) + if err != nil { + return err + } + } + + return node.StartLocalNode( + app, + clusterName, + globalNetworkFlags.UseEtnaDevnet, + avalanchegoBinaryPath, + numNodes, + partialSync, + nodeConfig, + anrSettings, + avaGoVersionSetting, + globalNetworkFlags, + createSupportedNetworkOptions, + ) +} + +func localStopNode(_ *cobra.Command, _ []string) error { + return node.StopLocalNode(app) +} + +func localDestroyNode(_ *cobra.Command, args []string) error { + clusterName := args[0] + return node.DestroyLocalNode(app, clusterName) +} + +func localTrack(_ *cobra.Command, args []string) error { + if avalanchegoBinaryPath == "" { + if useCustomAvalanchegoVersion != "" { + useLatestAvalanchegoReleaseVersion = false + useLatestAvalanchegoPreReleaseVersion = false + } + avaGoVersionSetting := node.AvalancheGoVersionSettings{ + UseCustomAvalanchegoVersion: useCustomAvalanchegoVersion, + UseLatestAvalanchegoPreReleaseVersion: useLatestAvalanchegoPreReleaseVersion, + UseLatestAvalanchegoReleaseVersion: useLatestAvalanchegoReleaseVersion, + } + avalancheGoVersion, err := node.GetAvalancheGoVersion(app, avaGoVersionSetting) + if err != nil { + return err + } + _, avagoDir, err := binutils.SetupAvalanchego(app, avalancheGoVersion) + if err != nil { + return fmt.Errorf("failed installing Avalanche Go version %s: %w", avalancheGoVersion, err) + } + avalanchegoBinaryPath = filepath.Join(avagoDir, "avalanchego") + } + return node.TrackSubnetWithLocalMachine(app, args[0], args[1], avalanchegoBinaryPath) +} + +func localStatus(_ *cobra.Command, args []string) error { + clusterName := "" + if len(args) > 0 { + clusterName = args[0] + } + if blockchainName != "" && clusterName == "" { + return fmt.Errorf("--blockchain flag is only supported if clusterName is specified") + } + return node.LocalStatus(app, clusterName, blockchainName) +} + +func notImplementedForLocal(what string) error { + ux.Logger.PrintToUser("Unsupported cmd: %s is not supported by local clusters", logging.LightBlue.Wrap(what)) + return nil +} diff --git a/cmd/nodecmd/node.go b/cmd/nodecmd/node.go index c67100453..e438470e8 100644 --- a/cmd/nodecmd/node.go +++ b/cmd/nodecmd/node.go @@ -58,5 +58,7 @@ rest of the commands to maintain your node and make your node a Subnet Validator cmd.AddCommand(newExportCmd()) // node import cmd.AddCommand(newImportCmd()) + // node local + cmd.AddCommand(newLocalCmd()) return cmd } diff --git a/cmd/nodecmd/refresh_ips.go b/cmd/nodecmd/refresh_ips.go index c6f9f21c2..f4d7bbde1 100644 --- a/cmd/nodecmd/refresh_ips.go +++ b/cmd/nodecmd/refresh_ips.go @@ -5,6 +5,8 @@ package nodecmd import ( "fmt" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/spf13/cobra" @@ -29,7 +31,7 @@ and updates the local node information used by CLI commands.`, func refreshIPs(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if err := failForExternal(clusterName); err != nil { diff --git a/cmd/nodecmd/resize.go b/cmd/nodecmd/resize.go index fe67b1557..b4a56efba 100644 --- a/cmd/nodecmd/resize.go +++ b/cmd/nodecmd/resize.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" + nodePkg "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" gcpAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/gcp" @@ -56,18 +58,25 @@ func preResizeChecks(clusterName string) error { if err := failForExternal(clusterName); err != nil { return fmt.Errorf("cannot resize external cluster %s", clusterName) } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + if clusterConfig.Local { + return notImplementedForLocal("resize") + } return nil } func resize(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := nodePkg.CheckCluster(app, clusterName); err != nil { return err } if err := preResizeChecks(clusterName); err != nil { return err } - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := nodePkg.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -105,7 +114,7 @@ func resize(_ *cobra.Command, args []string) error { if err != nil { return err } - if !(authorizeAccess || authorizedAccessFromSettings()) && (requestCloudAuth(nodeConfig.CloudService) != nil) { + if !(authorizeAccess || nodePkg.AuthorizedAccessFromSettings(app)) && (requestCloudAuth(nodeConfig.CloudService) != nil) { return fmt.Errorf("cloud access is required") } spinSession := ux.NewUserSpinner() diff --git a/cmd/nodecmd/scp.go b/cmd/nodecmd/scp.go index 44f6fc55b..b722e68c9 100644 --- a/cmd/nodecmd/scp.go +++ b/cmd/nodecmd/scp.go @@ -9,6 +9,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" @@ -55,13 +57,9 @@ $ avalanche node scp node1:/tmp/file.txt NodeID-XXXX:/tmp/file.txt } func scpNode(_ *cobra.Command, args []string) error { - var err error - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if len(clustersConfig.Clusters) == 0 { ux.Logger.PrintToUser("There are no clusters defined.") @@ -73,17 +71,26 @@ func scpNode(_ *cobra.Command, args []string) error { destClusterNameOrNodeID, destPath := utils.SplitSCPPath(destPath) // check if source and destination are both clusters - sourceClusterExists, err := checkClusterExists(sourceClusterNameOrNodeID) + sourceClusterExists, err := node.CheckClusterExists(app, sourceClusterNameOrNodeID) if err != nil { return err } - destClusterExists, err := checkClusterExists(destClusterNameOrNodeID) + destClusterExists, err := node.CheckClusterExists(app, destClusterNameOrNodeID) if err != nil { return err } if sourceClusterExists && destClusterExists { return fmt.Errorf("both source and destination cannot be clusters") } + sourceClusterConfig := clustersConfig.Clusters[sourceClusterNameOrNodeID] + if sourceClusterExists && sourceClusterConfig.Local { + return notImplementedForLocal("scp") + } + + destClusterConfig := clustersConfig.Clusters[destClusterNameOrNodeID] + if destClusterExists && destClusterConfig.Local { + return notImplementedForLocal("scp") + } switch { case sourceClusterExists: @@ -245,13 +252,9 @@ func prepareSCPTarget(op ClusterOp, host *models.Host, clusterName string, dest // getHostClusterPair returns the host and cluster name for the given node or cloudID func getHostClusterPair(nodeOrCloudIDOrIP string) (*models.Host, string) { - var err error - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return nil, "" - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return nil, "" } for clusterName := range clustersConfig.Clusters { clusterHosts, err := GetAllClusterHosts(clusterName) diff --git a/cmd/nodecmd/ssh.go b/cmd/nodecmd/ssh.go index b3b8d33b5..ee78f0961 100644 --- a/cmd/nodecmd/ssh.go +++ b/cmd/nodecmd/ssh.go @@ -10,6 +10,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -48,13 +50,9 @@ If no [cmd] is provided for the node, it will open ssh shell there. } func sshNode(_ *cobra.Command, args []string) error { - var err error - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return err } if len(clustersConfig.Clusters) == 0 { ux.Logger.PrintToUser("There are no clusters defined.") @@ -63,6 +61,9 @@ func sshNode(_ *cobra.Command, args []string) error { if len(args) == 0 { // provide ssh connection string for all clusters for clusterName, clusterConfig := range clustersConfig.Clusters { + if clusterConfig.Local { + continue + } err := printClusterConnectionString(clusterName, clusterConfig.Network.Kind.String()) if err != nil { return err @@ -72,11 +73,14 @@ func sshNode(_ *cobra.Command, args []string) error { } else { clusterNameOrNodeID := args[0] cmd := strings.Join(args[1:], " ") - if err := checkCluster(clusterNameOrNodeID); err == nil { + if err := node.CheckCluster(app, clusterNameOrNodeID); err == nil { // clusterName detected if len(args[1:]) == 0 { return printClusterConnectionString(clusterNameOrNodeID, clustersConfig.Clusters[clusterNameOrNodeID].Network.Kind.String()) } else { + if clustersConfig.Clusters[clusterNameOrNodeID].Local { + return notImplementedForLocal("ssh") + } clusterHosts, err := GetAllClusterHosts(clusterNameOrNodeID) if err != nil { return err @@ -219,7 +223,7 @@ func printClusterConnectionString(clusterName string, networkName string) error // GetAllClusterHosts returns all hosts in a cluster including loadtest and monitoring hosts func GetAllClusterHosts(clusterName string) ([]*models.Host, error) { - if exists, err := checkClusterExists(clusterName); err != nil || !exists { + if exists, err := node.CheckClusterExists(app, clusterName); err != nil || !exists { return nil, fmt.Errorf("cluster %s not found", clusterName) } clusterHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) diff --git a/cmd/nodecmd/status.go b/cmd/nodecmd/status.go index dc499cc18..9cae4bcf8 100644 --- a/cmd/nodecmd/status.go +++ b/cmd/nodecmd/status.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -50,13 +52,17 @@ func statusNode(_ *cobra.Command, args []string) error { return list(nil, nil) } clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clusterConf, err := app.GetClusterConfig(clusterName) if err != nil { return err } + // local cluster doesn't have nodes + if clusterConf.Local { + return notImplementedForLocal("status") + } var blockchainID ids.ID if blockchainName != "" { sc, err := app.LoadSidecar(blockchainName) @@ -68,6 +74,7 @@ func statusNode(_ *cobra.Command, args []string) error { return ErrNoBlockchainID } } + hostIDs := utils.Filter(clusterConf.GetCloudIDs(), clusterConf.IsAvalancheGoHost) nodeIDs, err := utils.MapWithError(hostIDs, func(s string) (string, error) { n, err := getNodeID(app.GetNodeInstanceDirPath(s)) @@ -87,11 +94,11 @@ func statusNode(_ *cobra.Command, args []string) error { if err != nil { return err } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) spinSession := ux.NewUserSpinner() spinner := spinSession.SpinToUser("Checking node(s) status...") - notBootstrappedNodes, err := getNotBootstrappedNodes(hosts) + notBootstrappedNodes, err := node.GetNotBootstrappedNodes(hosts) if err != nil { ux.SpinFailWithError(spinner, "", err) return err @@ -99,7 +106,7 @@ func statusNode(_ *cobra.Command, args []string) error { ux.SpinComplete(spinner) spinner = spinSession.SpinToUser("Checking if node(s) are healthy...") - unhealthyNodes, err := getUnhealthyNodes(hosts) + unhealthyNodes, err := node.GetUnhealthyNodes(hosts) if err != nil { ux.SpinFailWithError(spinner, "", err) return err @@ -117,7 +124,7 @@ func statusNode(_ *cobra.Command, args []string) error { nodeResults.AddResult(host.GetCloudID(), nil, err) return } else { - if avalancheGoVersion, _, err := parseAvalancheGoOutput(resp); err != nil { + if avalancheGoVersion, _, err := node.ParseAvalancheGoOutput(resp); err != nil { nodeResults.AddResult(host.GetCloudID(), nil, err) } else { nodeResults.AddResult(host.GetCloudID(), avalancheGoVersion, err) diff --git a/cmd/nodecmd/sync.go b/cmd/nodecmd/sync.go index ea5c8a42a..134117739 100644 --- a/cmd/nodecmd/sync.go +++ b/cmd/nodecmd/sync.go @@ -3,17 +3,8 @@ package nodecmd import ( - "fmt" - "sync" - - "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" - "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" - "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/ssh" - "github.com/ava-labs/avalanche-cli/pkg/utils" - "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanche-cli/pkg/node" "github.com/spf13/cobra" ) @@ -39,157 +30,5 @@ You can check the blockchain bootstrap status by calling avalanche node status < func syncSubnet(_ *cobra.Command, args []string) error { clusterName := args[0] blockchainName := args[1] - if err := checkCluster(clusterName); err != nil { - return err - } - clusterConfig, err := app.GetClusterConfig(clusterName) - if err != nil { - return err - } - if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{blockchainName}); err != nil { - return err - } - hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) - if err != nil { - return err - } - if len(validators) != 0 { - hosts, err = filterHosts(hosts, validators) - if err != nil { - return err - } - } - defer disconnectHosts(hosts) - if !avoidChecks { - if err := checkHostsAreBootstrapped(hosts); err != nil { - return err - } - if err := checkHostsAreHealthy(hosts); err != nil { - return err - } - if err := checkHostsAreRPCCompatible(hosts, blockchainName); err != nil { - return err - } - } - if err := prepareSubnetPlugin(hosts, blockchainName); err != nil { - return err - } - if err := trackSubnet(hosts, clusterName, clusterConfig.Network, blockchainName); err != nil { - return err - } - ux.Logger.PrintToUser("Node(s) successfully started syncing with Blockchain!") - ux.Logger.PrintToUser(fmt.Sprintf("Check node blockchain syncing status with avalanche node status %s --blockchain %s", clusterName, blockchainName)) - return nil -} - -// prepareSubnetPlugin creates subnet plugin to all nodes in the cluster -func prepareSubnetPlugin(hosts []*models.Host, blockchainName string) error { - sc, err := app.LoadSidecar(blockchainName) - if err != nil { - return err - } - wg := sync.WaitGroup{} - wgResults := models.NodeResults{} - for _, host := range hosts { - wg.Add(1) - go func(nodeResults *models.NodeResults, host *models.Host) { - defer wg.Done() - if err := ssh.RunSSHCreatePlugin(host, sc); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return fmt.Errorf("failed to upload plugin to node(s) %s", wgResults.GetErrorHostMap()) - } - return nil -} - -// trackSubnet exports deployed subnet in user's local machine to cloud server and calls node to -// start tracking the specified subnet (similar to avalanche subnet join command) -func trackSubnet( - hosts []*models.Host, - clusterName string, - network models.Network, - blockchainName string, -) error { - // load cluster config - clusterConfig, err := app.GetClusterConfig(clusterName) - if err != nil { - return err - } - // and get list of subnets - allSubnets := utils.Unique(append(clusterConfig.Subnets, blockchainName)) - - // load sidecar to get subnet blockchain ID - sc, err := app.LoadSidecar(blockchainName) - if err != nil { - return err - } - blockchainID := sc.Networks[network.Name()].BlockchainID - - wg := sync.WaitGroup{} - wgResults := models.NodeResults{} - for _, host := range hosts { - wg.Add(1) - go func(nodeResults *models.NodeResults, host *models.Host) { - defer wg.Done() - if err := ssh.RunSSHStopNode(host); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - - if err := ssh.RunSSHRenderAvagoAliasConfigFile( - host, - blockchainID.String(), - subnetAliases, - ); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - if err := ssh.RunSSHRenderAvalancheNodeConfig( - app, - host, - network, - allSubnets, - clusterConfig.IsAPIHost(host.GetCloudID()), - ); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - if err := ssh.RunSSHSyncSubnetData(app, host, network, blockchainName); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - } - if err := ssh.RunSSHStartNode(host); err != nil { - nodeResults.AddResult(host.NodeID, nil, err) - return - } - }(&wgResults, host) - } - wg.Wait() - if wgResults.HasErrors() { - return fmt.Errorf("failed to track subnet for node(s) %s", wgResults.GetErrorHostMap()) - } - - // update slice of subnets synced by the cluster - clusterConfig.Subnets = allSubnets - err = app.SetClusterConfig(network.ClusterName, clusterConfig) - if err != nil { - return err - } - - // update slice of blockchain endpoints with the cluster ones - networkInfo := sc.Networks[clusterConfig.Network.Name()] - rpcEndpoints := set.Of(networkInfo.RPCEndpoints...) - wsEndpoints := set.Of(networkInfo.WSEndpoints...) - publicEndpoints, err := getPublicEndpoints(clusterName) - if err != nil { - return err - } - for _, publicEndpoint := range publicEndpoints { - rpcEndpoints.Add(getRPCEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) - wsEndpoints.Add(getWSEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) - } - networkInfo.RPCEndpoints = rpcEndpoints.List() - networkInfo.WSEndpoints = wsEndpoints.List() - sc.Networks[clusterConfig.Network.Name()] = networkInfo - return app.UpdateSidecar(&sc) + return node.SyncSubnet(app, clusterName, blockchainName, avoidChecks, subnetAliases) } diff --git a/cmd/nodecmd/update_subnet.go b/cmd/nodecmd/update_subnet.go index 810aaf5c0..8a71328f8 100644 --- a/cmd/nodecmd/update_subnet.go +++ b/cmd/nodecmd/update_subnet.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -34,13 +36,16 @@ You can check the updated subnet bootstrap status by calling avalanche node stat func updateSubnet(_ *cobra.Command, args []string) error { clusterName := args[0] subnetName := args[1] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clusterConfig, err := app.GetClusterConfig(clusterName) if err != nil { return err } + if clusterConfig.Local { + return notImplementedForLocal("update") + } if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { return err } @@ -48,14 +53,14 @@ func updateSubnet(_ *cobra.Command, args []string) error { if err != nil { return err } - defer disconnectHosts(hosts) - if err := checkHostsAreBootstrapped(hosts); err != nil { + defer node.DisconnectHosts(hosts) + if err := node.CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } - if err := checkHostsAreRPCCompatible(hosts, subnetName); err != nil { + if err := node.CheckHostsAreRPCCompatible(app, hosts, subnetName); err != nil { return err } nonUpdatedNodes, err := doUpdateSubnet(hosts, clusterName, clusterConfig.Network, subnetName) diff --git a/cmd/nodecmd/upgrade.go b/cmd/nodecmd/upgrade.go index 0a8d3200a..e07c5d083 100644 --- a/cmd/nodecmd/upgrade.go +++ b/cmd/nodecmd/upgrade.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -47,19 +49,21 @@ You can check the status after upgrade by calling avalanche node status`, func upgrade(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clusterConfig, err := app.GetClusterConfig(clusterName) if err != nil { return err } - network := clusterConfig.Network + if clusterConfig.Local { + return notImplementedForLocal("upgrade") + } hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) if err != nil { return err } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) toUpgradeNodesMap, err := getNodesUpgradeInfo(hosts) if err != nil { return err @@ -68,9 +72,7 @@ func upgrade(_ *cobra.Command, args []string) error { for host, upgradeInfo := range toUpgradeNodesMap { if upgradeInfo.AvalancheGoVersion != "" { spinner := spinSession.SpinToUser(utils.ScriptLog(host.NodeID, fmt.Sprintf("Upgrading avalanchego to version %s...", upgradeInfo.AvalancheGoVersion))) - // check if host is API host - publicAccessToHTTPPort := clusterConfig.IsAPIHost(host.GetCloudID()) || clusterConfig.HTTPAccess == constants.PublicAccess - if err := upgradeAvalancheGo(host, network, upgradeInfo.AvalancheGoVersion, publicAccessToHTTPPort); err != nil { + if err := upgradeAvalancheGo(host, upgradeInfo.AvalancheGoVersion); err != nil { ux.SpinFailWithError(spinner, "", err) return err } @@ -216,11 +218,9 @@ func checkIfKeyIsStandardVMName(vmName string) bool { func upgradeAvalancheGo( host *models.Host, - network models.Network, avaGoVersionToUpdateTo string, - publicAccessToHTTPPort bool, ) error { - if err := ssh.RunSSHUpgradeAvalanchego(host, network, avaGoVersionToUpdateTo, publicAccessToHTTPPort); err != nil { + if err := ssh.RunSSHUpgradeAvalanchego(host, avaGoVersionToUpdateTo); err != nil { return err } return nil diff --git a/cmd/nodecmd/upgrade.json b/cmd/nodecmd/upgrade.json new file mode 100644 index 000000000..ca3a7f13a --- /dev/null +++ b/cmd/nodecmd/upgrade.json @@ -0,0 +1,16 @@ +{ + "apricotPhase1Time": "2020-12-05T05:00:00Z", + "apricotPhase2Time": "2020-12-05T05:00:00Z", + "apricotPhase3Time": "2020-12-05T05:00:00Z", + "apricotPhase4Time": "2020-12-05T05:00:00Z", + "apricotPhase4MinPChainHeight": 0, + "apricotPhase5Time": "2020-12-05T05:00:00Z", + "apricotPhasePre6Time": "2020-12-05T05:00:00Z", + "apricotPhase6Time": "2020-12-05T05:00:00Z", + "apricotPhasePost6Time": "2020-12-05T05:00:00Z", + "banffTime": "2020-12-05T05:00:00Z", + "cortinaTime": "2020-12-05T05:00:00Z", + "cortinaXChainStopVertexID": "11111111111111111111111111111111LpoYY", + "durangoTime": "2020-12-05T05:00:00Z", + "etnaTime": "2020-12-05T05:00:00Z" +} diff --git a/cmd/nodecmd/validate_primary.go b/cmd/nodecmd/validate_primary.go index 44f1f8fec..ddf755492 100644 --- a/cmd/nodecmd/validate_primary.go +++ b/cmd/nodecmd/validate_primary.go @@ -9,6 +9,8 @@ import ( "strconv" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + blockchaincmd "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -287,7 +289,7 @@ func addNodeAsPrimaryNetworkValidator( func validatePrimaryNetwork(_ *cobra.Command, args []string) error { clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } @@ -295,6 +297,9 @@ func validatePrimaryNetwork(_ *cobra.Command, args []string) error { if err != nil { return err } + if clusterConfig.Local { + return notImplementedForLocal("validate primary") + } network := clusterConfig.Network allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) @@ -302,7 +307,7 @@ func validatePrimaryNetwork(_ *cobra.Command, args []string) error { return err } hosts := clusterConfig.GetValidatorHosts(allHosts) // exlude api nodes - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.AddPrimaryNetworkValidatorFee * uint64(len(hosts)) kc, err := keychain.GetKeychainFromCmdLineFlags( @@ -321,10 +326,10 @@ func validatePrimaryNetwork(_ *cobra.Command, args []string) error { deployer := subnet.NewPublicDeployer(app, kc, network) - if err := checkHostsAreBootstrapped(hosts); err != nil { + if err := node.CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } diff --git a/cmd/nodecmd/validate_subnet.go b/cmd/nodecmd/validate_subnet.go index ebc952a5e..e73138d2d 100644 --- a/cmd/nodecmd/validate_subnet.go +++ b/cmd/nodecmd/validate_subnet.go @@ -8,6 +8,8 @@ import ( "fmt" "time" + "github.com/ava-labs/avalanche-cli/pkg/node" + blockchaincmd "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -96,7 +98,7 @@ func addNodeAsSubnetValidator( } ux.Logger.PrintToUser("Adding the node as a Subnet Validator...") defer ux.Logger.PrintLineSeparator() - if err := blockchaincmd.CallAddValidator( + if err := blockchaincmd.CallAddValidatorNonSOV( deployer, network, kc, @@ -173,7 +175,7 @@ func validateSubnet(_ *cobra.Command, args []string) error { clusterName := args[0] subnetName := args[1] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if _, err := blockchaincmd.ValidateSubnetNameAndGetChains([]string{subnetName}); err != nil { @@ -184,6 +186,9 @@ func validateSubnet(_ *cobra.Command, args []string) error { if err != nil { return err } + if clusterConfig.Local { + return notImplementedForLocal("validate subnet") + } network := clusterConfig.Network allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) @@ -197,7 +202,7 @@ func validateSubnet(_ *cobra.Command, args []string) error { return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) nodeIDMap, failedNodesMap := getNodeIDs(hosts) nonPrimaryValidators := 0 @@ -232,10 +237,10 @@ func validateSubnet(_ *cobra.Command, args []string) error { deployer := subnet.NewPublicDeployer(app, kc, network) if !avoidChecks { - if err := checkHostsAreBootstrapped(hosts); err != nil { + if err := node.CheckHostsAreBootstrapped(hosts); err != nil { return err } - if err := checkHostsAreHealthy(hosts); err != nil { + if err := node.CheckHostsAreHealthy(hosts); err != nil { return err } } diff --git a/cmd/nodecmd/whitelist.go b/cmd/nodecmd/whitelist.go index d7ec66172..27a56b94e 100644 --- a/cmd/nodecmd/whitelist.go +++ b/cmd/nodecmd/whitelist.go @@ -8,6 +8,8 @@ import ( "strings" "sync" + "github.com/ava-labs/avalanche-cli/pkg/node" + "github.com/ava-labs/avalanche-cli/pkg/ansible" "github.com/ava-labs/avalanche-cli/pkg/application" awsAPI "github.com/ava-labs/avalanche-cli/pkg/cloud/aws" @@ -61,13 +63,21 @@ type regionSecurityGroup struct { func whitelist(_ *cobra.Command, args []string) error { var err error clusterName := args[0] - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } if err := failForExternal(clusterName); err != nil { return err } + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return err + } + clusterConfig := clustersConfig.Clusters[clusterName] + if clusterConfig.Local { + return notImplementedForLocal("whitelist") + } if discoverIP { userIPAddress, err = utils.GetUserIPAddress() if err != nil { @@ -116,7 +126,7 @@ func whitelist(_ *cobra.Command, args []string) error { if userIPAddress != "" { ux.Logger.GreenCheckmarkToUser("Whitelisting IP: %s", logging.LightBlue.Wrap(userIPAddress)) cloudSecurityGroupList := []regionSecurityGroup{} - clusterNodes, err := getClusterNodes(clusterName) + clusterNodes, err := node.GetClusterNodes(app, clusterName) if err != nil { return err } @@ -234,7 +244,7 @@ func GrantAccessToIPinGCP(userIPAddress string) error { func whitelistSSHPubKey(clusterName string, pubkey string) error { sshPubKey := strings.Trim(pubkey, "\"'") - if err := checkCluster(clusterName); err != nil { + if err := node.CheckCluster(app, clusterName); err != nil { return err } clustersConfig, err := app.LoadClustersConfig() diff --git a/cmd/nodecmd/wiz.go b/cmd/nodecmd/wiz.go index 9532b3bca..45155dd51 100644 --- a/cmd/nodecmd/wiz.go +++ b/cmd/nodecmd/wiz.go @@ -257,7 +257,7 @@ func wiz(cmd *cobra.Command, args []string) error { } } - if err := waitForHealthyCluster(clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { + if err := node.WaitForHealthyCluster(app, clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { return err } @@ -354,7 +354,7 @@ func wiz(cmd *cobra.Command, args []string) error { if err := syncSubnet(cmd, []string{clusterName, subnetName}); err != nil { return err } - if err := waitForHealthyCluster(clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { + if err := node.WaitForHealthyCluster(app, clusterName, healthCheckTimeout, healthCheckPoolTime); err != nil { return err } blockchainID := sc.Networks[network.Name()].BlockchainID @@ -400,6 +400,7 @@ func wiz(cmd *cobra.Command, args []string) error { }, DeployMessenger: deployTeleporterMessenger, DeployRegistry: deployTeleporterRegistry, + ForceRegistryDeploy: true, Version: teleporterVersion, MessengerContractAddressPath: teleporterMessengerContractAddressPath, MessengerDeployerAddressPath: teleporterMessengerDeployerAddressPath, @@ -636,59 +637,8 @@ func checkRPCCompatibility( return err } } - defer disconnectHosts(hosts) - return checkHostsAreRPCCompatible(hosts, subnetName) -} - -func waitForHealthyCluster( - clusterName string, - timeout time.Duration, - poolTime time.Duration, -) error { - ux.Logger.PrintToUser("") - ux.Logger.PrintToUser("Waiting for node(s) in cluster %s to be healthy...", clusterName) - clustersConfig, err := app.LoadClustersConfig() - if err != nil { - return err - } - cluster, ok := clustersConfig.Clusters[clusterName] - if !ok { - return fmt.Errorf("cluster %s does not exist", clusterName) - } - allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) - if err != nil { - return err - } - hosts := cluster.GetValidatorHosts(allHosts) // exlude api nodes - defer disconnectHosts(hosts) - startTime := time.Now() - spinSession := ux.NewUserSpinner() - spinner := spinSession.SpinToUser("Checking if node(s) are healthy...") - for { - unhealthyNodes, err := getUnhealthyNodes(hosts) - if err != nil { - ux.SpinFailWithError(spinner, "", err) - return err - } - if len(unhealthyNodes) == 0 { - ux.SpinComplete(spinner) - spinSession.Stop() - ux.Logger.GreenCheckmarkToUser("Nodes healthy after %d seconds", uint32(time.Since(startTime).Seconds())) - return nil - } - if time.Since(startTime) > timeout { - ux.SpinFailWithError(spinner, "", fmt.Errorf("cluster not healthy after %d seconds", uint32(timeout.Seconds()))) - spinSession.Stop() - ux.Logger.PrintToUser("") - ux.Logger.RedXToUser("Unhealthy Nodes") - for _, failedNode := range unhealthyNodes { - ux.Logger.PrintToUser(" " + failedNode) - } - ux.Logger.PrintToUser("") - return fmt.Errorf("cluster not healthy after %d seconds", uint32(timeout.Seconds())) - } - time.Sleep(poolTime) - } + defer node.DisconnectHosts(hosts) + return node.CheckHostsAreRPCCompatible(app, hosts, subnetName) } func waitForSubnetValidators( @@ -714,7 +664,7 @@ func waitForSubnetValidators( return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) nodeIDMap, failedNodesMap := getNodeIDs(hosts) startTime := time.Now() for { @@ -781,7 +731,7 @@ func waitForClusterSubnetStatus( return err } } - defer disconnectHosts(hosts) + defer node.DisconnectHosts(hosts) startTime := time.Now() for { wg := sync.WaitGroup{} diff --git a/cmd/primarycmd/add_validator.go b/cmd/primarycmd/add_validator.go index e3a017654..73ae8c018 100644 --- a/cmd/primarycmd/add_validator.go +++ b/cmd/primarycmd/add_validator.go @@ -158,7 +158,7 @@ func addValidator(_ *cobra.Command, _ []string) error { } if nodeIDStr == "" { - nodeID, err = blockchaincmd.PromptNodeID() + nodeID, err = blockchaincmd.PromptNodeID("add as Primary Network Validator") if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 00ea688a2..327095e2e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,9 +6,11 @@ import ( "errors" "fmt" "os" + "os/signal" "os/user" "path/filepath" "strings" + "syscall" "time" "github.com/ava-labs/avalanche-cli/cmd/backendcmd" @@ -35,6 +37,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/perms" + ansi "github.com/k0kubun/go-ansi" "github.com/spf13/cobra" "go.uber.org/zap" @@ -247,8 +250,7 @@ func setupEnv() (string, error) { baseDir := filepath.Join(usr.HomeDir, constants.BaseDirName) // Create base dir if it doesn't exist - err = os.MkdirAll(baseDir, os.ModePerm) - if err != nil { + if err := os.MkdirAll(baseDir, os.ModePerm); err != nil { // no logger here yet fmt.Printf("failed creating the basedir %s: %s\n", baseDir, err) return "", err @@ -258,41 +260,49 @@ func setupEnv() (string, error) { snapshotsDir := filepath.Join(baseDir, constants.SnapshotsDirName) if err = os.MkdirAll(snapshotsDir, os.ModePerm); err != nil { fmt.Printf("failed creating the snapshots dir %s: %s\n", snapshotsDir, err) - os.Exit(1) + return "", err } // Create key dir if it doesn't exist keyDir := filepath.Join(baseDir, constants.KeyDir) if err = os.MkdirAll(keyDir, os.ModePerm); err != nil { fmt.Printf("failed creating the key dir %s: %s\n", keyDir, err) - os.Exit(1) + return "", err } // Create custom vm dir if it doesn't exist vmDir := filepath.Join(baseDir, constants.CustomVMDir) if err = os.MkdirAll(vmDir, os.ModePerm); err != nil { fmt.Printf("failed creating the vm dir %s: %s\n", vmDir, err) - os.Exit(1) + return "", err } // Create subnet dir if it doesn't exist subnetDir := filepath.Join(baseDir, constants.SubnetDir) if err = os.MkdirAll(subnetDir, os.ModePerm); err != nil { fmt.Printf("failed creating the subnet dir %s: %s\n", subnetDir, err) - os.Exit(1) + return "", err } // Create repos dir if it doesn't exist repoDir := filepath.Join(baseDir, constants.ReposDir) if err = os.MkdirAll(repoDir, os.ModePerm); err != nil { fmt.Printf("failed creating the repo dir %s: %s\n", repoDir, err) - os.Exit(1) + return "", err } + // Create nodes dir if it doesn't exist + nodesDir := filepath.Join(baseDir, constants.NodesDir) + if err = os.MkdirAll(nodesDir, os.ModePerm); err != nil { + fmt.Printf("failed creating the nodes dir %s: %s\n", nodesDir, err) + return "", err + } + + // Create plugin dir if it doesn't exist pluginDir := filepath.Join(baseDir, constants.PluginDir) if err = os.MkdirAll(pluginDir, os.ModePerm); err != nil { fmt.Printf("failed creating the plugin dir %s: %s\n", pluginDir, err) - os.Exit(1) + return "", err } return baseDir, nil @@ -347,8 +357,18 @@ func initConfig() { // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { + go handleInterrupt() app = application.New() rootCmd := NewRootCmd() err := rootCmd.Execute() cobrautils.HandleErrors(err) } + +func handleInterrupt() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + sig := <-sigChan + fmt.Println() + fmt.Println("received signal:", sig.String()) + ansi.CursorShow() +} diff --git a/cmd/teleportercmd/deploy.go b/cmd/teleportercmd/deploy.go index 99715f997..8d70588ec 100644 --- a/cmd/teleportercmd/deploy.go +++ b/cmd/teleportercmd/deploy.go @@ -25,6 +25,7 @@ type DeployFlags struct { GenesisKey bool DeployMessenger bool DeployRegistry bool + ForceRegistryDeploy bool RPCURL string Version string MessengerContractAddressPath string @@ -63,6 +64,7 @@ func newDeployCmd() *cobra.Command { deployFlags.ChainFlags.AddToCmd(cmd, "deploy ICM into %s") cmd.Flags().BoolVar(&deployFlags.DeployMessenger, "deploy-messenger", true, "deploy Teleporter Messenger") cmd.Flags().BoolVar(&deployFlags.DeployRegistry, "deploy-registry", true, "deploy Teleporter Registry") + cmd.Flags().BoolVar(&deployFlags.ForceRegistryDeploy, "force-registry-deploy", false, "deploy Teleporter Registry even if Messenger has already been deployed") cmd.Flags().StringVar(&deployFlags.RPCURL, "rpc-url", "", "use the given RPC URL to connect to the subnet") cmd.Flags().StringVar(&deployFlags.Version, "version", "latest", "version to deploy") cmd.Flags().StringVar(&deployFlags.MessengerContractAddressPath, "messenger-contract-address-path", "", "path to a messenger contract address file") @@ -187,6 +189,7 @@ func CallDeploy(_ []string, flags DeployFlags) error { privateKey, flags.DeployMessenger, flags.DeployRegistry, + flags.ForceRegistryDeploy, ) if err != nil { return err @@ -223,6 +226,7 @@ func CallDeploy(_ []string, flags DeployFlags) error { ewoq.PrivKeyHex(), flags.DeployMessenger, flags.DeployRegistry, + false, ) if err != nil { return err diff --git a/cmd/transactioncmd/transaction_commit.go b/cmd/transactioncmd/transaction_commit.go index 21712c924..9a04b81e6 100644 --- a/cmd/transactioncmd/transaction_commit.go +++ b/cmd/transactioncmd/transaction_commit.go @@ -97,7 +97,7 @@ func commitTx(_ *cobra.Command, args []string) error { if err := blockchaincmd.PrintDeployResults(subnetName, subnetID, txID); err != nil { return err } - return app.UpdateSidecarNetworks(&sc, network, subnetID, txID, "", "") + return app.UpdateSidecarNetworks(&sc, network, subnetID, txID, "", "", sc.Networks[network.Name()].BootstrapValidators, "") } return nil diff --git a/go.mod b/go.mod index b03fcc54b..80fea607c 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.22.8 require ( github.com/ava-labs/apm v1.0.0 - github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241003175856-6b1bcdf33e7a - github.com/ava-labs/avalanchego v1.11.13-0.20241016194412-e5ca05301169 - github.com/ava-labs/awm-relayer v1.3.0 - github.com/ava-labs/coreth v0.13.8 - github.com/ava-labs/subnet-evm v0.6.11 - github.com/aws/aws-sdk-go-v2 v1.32.2 - github.com/aws/aws-sdk-go-v2/config v1.27.26 + github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241120013844-71784ad023cc + github.com/ava-labs/avalanchego v1.12.0-fuji + github.com/ava-labs/awm-relayer v1.4.1-0.20241119163059-6abfe81abee0 + github.com/ava-labs/coreth v0.13.9-rc.1 + github.com/ava-labs/subnet-evm v0.6.12 + github.com/aws/aws-sdk-go-v2 v1.32.5 + github.com/aws/aws-sdk-go-v2/config v1.28.5 github.com/aws/aws-sdk-go-v2/service/ec2 v1.182.0 github.com/chelnak/ysmrr v0.4.0 github.com/docker/docker v27.1.1+incompatible @@ -18,6 +18,7 @@ require ( github.com/fatih/color v1.17.0 github.com/go-git/go-git/v5 v5.12.0 github.com/jedib0t/go-pretty/v6 v6.5.9 + github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/liyue201/erc20-go v0.0.0-20210521034206-b2824246def0 github.com/manifoldco/promptui v0.9.0 @@ -25,26 +26,29 @@ require ( github.com/mitchellh/go-wordwrap v1.0.1 github.com/okteto/remote v0.0.0-20210428052247-99de42c04148 github.com/olekukonko/tablewriter v0.0.5 - github.com/onsi/ginkgo/v2 v2.20.2 - github.com/onsi/gomega v1.34.1 + github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/gomega v1.35.1 github.com/pborman/ansi v1.0.0 github.com/pingcap/errors v0.11.4 github.com/posthog/posthog-go v1.2.24 + github.com/prometheus/client_golang v1.20.5 + github.com/schollz/progressbar/v3 v3.16.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/afero v1.11.0 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + go.uber.org/mock v0.5.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/mod v0.21.0 - golang.org/x/net v0.28.0 - golang.org/x/oauth2 v0.21.0 - golang.org/x/sync v0.8.0 - golang.org/x/text v0.17.0 + golang.org/x/net v0.30.0 + golang.org/x/oauth2 v0.23.0 + golang.org/x/sync v0.9.0 + golang.org/x/text v0.19.0 google.golang.org/api v0.184.0 - google.golang.org/protobuf v1.35.1 + google.golang.org/protobuf v1.35.2 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -52,7 +56,7 @@ require ( require ( cloud.google.com/go/auth v0.5.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/DataDog/zstd v1.5.2 // indirect github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect @@ -63,19 +67,19 @@ require ( github.com/VictoriaMetrics/fastcache v1.12.1 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60 // indirect - github.com/ava-labs/teleporter v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.26 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.31.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect - github.com/aws/smithy-go v1.22.0 // indirect + github.com/ava-labs/teleporter v1.0.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect @@ -92,7 +96,7 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect github.com/creack/pty v1.1.11 // indirect @@ -110,7 +114,6 @@ require ( github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/gliderlabs/ssh v0.3.7 // indirect - github.com/go-cmd/cmd v1.4.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -125,7 +128,7 @@ require ( github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect @@ -138,6 +141,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect @@ -149,18 +153,20 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect github.com/otiai10/copy v1.11.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect @@ -169,11 +175,10 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.6 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -188,7 +193,7 @@ require ( github.com/status-im/keycard-go v0.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/supranational/blst v0.3.11 // indirect + github.com/supranational/blst v0.3.13 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect @@ -210,16 +215,15 @@ require ( go.opentelemetry.io/otel/sdk v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.26.0 // indirect gonum.org/v1/gonum v0.11.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect - google.golang.org/grpc v1.66.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.68.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index a06f1a047..f3e5ab8bc 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRk cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= @@ -83,59 +83,59 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/ava-labs/apm v1.0.0 h1:6FwozH67hEkbWVsOXNZGexBy5KLpNeYucN9zcFUHv+Q= github.com/ava-labs/apm v1.0.0/go.mod h1:TJL7pTlZNvQatsQPsLUtDHApEwVZ/qS7iSNtRFU83mc= -github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241003175856-6b1bcdf33e7a h1:xd2RUTW9w34+V6yI7zWfByHZ8y5CeXhvHK+nAJ9FCbI= -github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241003175856-6b1bcdf33e7a/go.mod h1:l4QzFnujbyyyeq6oBQ4F6sw9TrTQCjD2V4vUd7ZBCCo= -github.com/ava-labs/avalanchego v1.11.13-0.20241016194412-e5ca05301169 h1:v09QsEUbm5fjPgDWEJZzSFU7T4b0SPiKyZq5DvR8rSU= -github.com/ava-labs/avalanchego v1.11.13-0.20241016194412-e5ca05301169/go.mod h1:YlinMYWE4yXu4bA5Vdzn1WcQ+z5qjrpxeBYwWAPdrvA= -github.com/ava-labs/awm-relayer v1.3.0 h1:aI90Daoq7bs9SnfZpyjXajj7YLcdZoFgEkyCd52fOHE= -github.com/ava-labs/awm-relayer v1.3.0/go.mod h1:UI1Tm0jfFIpO1S2hpE5WGzJCDpLHLldhsE1bWn2FjEE= -github.com/ava-labs/coreth v0.13.8 h1:f14X3KgwHl9LwzfxlN6S4bbn5VA2rhEsNnHaRLSTo/8= -github.com/ava-labs/coreth v0.13.8/go.mod h1:t3BSv/eQv0AlDPMfEDCMMoD/jq1RkUsbFzQAFg5qBcE= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241120013844-71784ad023cc h1:fd19g51oBgrolLOHJ9aGbJlf8D2lizxtjSINqC71AL8= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241120013844-71784ad023cc/go.mod h1:TOfc8r6sxs01mi2mWOYsRcJAbf3x3a4x0uxi8gVThso= +github.com/ava-labs/avalanchego v1.12.0-fuji h1:o/GbXrqW9CAXu2jX/a1dZtvFiiSVCWomJZyxF4hCQOA= +github.com/ava-labs/avalanchego v1.12.0-fuji/go.mod h1:yhD5dpZyStIVbxQ550EDi5w5SL7DQ/xGE6TIxosb7U0= +github.com/ava-labs/awm-relayer v1.4.1-0.20241119163059-6abfe81abee0 h1:/dI9oPOYdkdSiUr9byeKz+bZ4onXrBjZQuXqDtPtSuY= +github.com/ava-labs/awm-relayer v1.4.1-0.20241119163059-6abfe81abee0/go.mod h1:cLaEdx57PXwDjebIvKmY7aSGqTivAZLD0vrkXEmISvc= +github.com/ava-labs/coreth v0.13.9-rc.1 h1:qIICpC/OZGYUP37QnLgIqqwGmxnLwLpZaUlqJNI85vU= +github.com/ava-labs/coreth v0.13.9-rc.1/go.mod h1:7aMsRIo/3GBE44qWZMjnfqdqfcfZ5yShTTm2LObLaYo= github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60 h1:EL66gtXOAwR/4KYBjOV03LTWgkEXvLePribLlJNu4g0= github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60/go.mod h1:/7qKobTfbzBu7eSTVaXMTr56yTYk4j2Px6/8G+idxHo= -github.com/ava-labs/subnet-evm v0.6.11 h1:XgIAn9ihPyGjrBFD2rXg2H76HFl6htliHabN0fjciaQ= -github.com/ava-labs/subnet-evm v0.6.11/go.mod h1:fP46lzBtiniRfpNH+Qu9BFvSsa02YGkAobxinZ78gDU= -github.com/ava-labs/teleporter v1.0.0 h1:io209qZh3SDpwLre0oStMzOFRcPvOrmMQuLq5OFvzJo= -github.com/ava-labs/teleporter v1.0.0/go.mod h1:4Wyz/5sZDHMaaLegh2ULyrAOWnyaBk6upTmbwSrVSMs= +github.com/ava-labs/subnet-evm v0.6.12 h1:jL3FmjdFcNfS0qwbehwN6DkAg9y7zexB1riiGBxRsM0= +github.com/ava-labs/subnet-evm v0.6.12/go.mod h1:vffwL4UqAh7ibpWjveUuUhamm3a9w75q92bG5vXdX5k= +github.com/ava-labs/teleporter v1.0.7 h1:9H0wTWhgWeA4u6uxi9KngdBu/LOoYsfAmIZhXNzuvf8= +github.com/ava-labs/teleporter v1.0.7/go.mod h1:wgCgU6vU5MtP83otpjEin8jL2jrflVBNQCTxVXMx/kU= github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= -github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= -github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= +github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= -github.com/aws/aws-sdk-go-v2/config v1.27.26 h1:T1kAefbKuNum/AbShMsZEro6eRkeOT8YILfE9wyjAYQ= -github.com/aws/aws-sdk-go-v2/config v1.27.26/go.mod h1:ivWHkAWFrw/nxty5Fku7soTIVdqZaZ7dw+tc5iGW3GA= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= -github.com/aws/aws-sdk-go-v2/credentials v1.17.26 h1:tsm8g/nJxi8+/7XyJJcP2dLrnK/5rkFp6+i2nhmz5fk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.26/go.mod h1:3vAM49zkIa3q8WT6o9Ve5Z0vdByDMwmdScO0zvThTgI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/ec2 v1.182.0 h1:LaeziEhHZ/SJZYBK223QVzl3ucHvA9IP4tQMcxGrc9I= github.com/aws/aws-sdk-go-v2/service/ec2 v1.182.0/go.mod h1:kYXaB4FzyhEJjvrJ84oPnMElLiEAjGxxUunVW2tBSng= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= -github.com/aws/aws-sdk-go-v2/service/kms v1.31.0 h1:yl7wcqbisxPzknJVfWTLnK83McUvXba+pz2+tPbIUmQ= -github.com/aws/aws-sdk-go-v2/service/kms v1.31.0/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.6 h1:CZImQdb1QbU9sGgJ9IswhVkxAcjkkD1eQTMA1KHWk+E= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.6/go.mod h1:YJDdlK0zsyxVBxGU48AR/Mi8DMrGdc1E3Yij4fNrONA= github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.3 h1:Fv1vD2L65Jnp5QRsdiM64JvUM4Xe+E0JyVsRQKv6IeA= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.3/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -185,6 +185,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chelnak/ysmrr v0.4.0 h1:WMvLGPlBK0kb6wHf5z9FfNvpM6sB9765jy2ajYc1Sfs= github.com/chelnak/ysmrr v0.4.0/go.mod h1:8vCna4PJsPCb6eevtoG7Tljzfx3twpsO203Qj2gafLM= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= @@ -232,8 +234,8 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= @@ -330,8 +332,6 @@ github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7 github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc= -github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= @@ -366,8 +366,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= -github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= @@ -389,8 +387,8 @@ github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= -github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -450,8 +448,8 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= @@ -489,6 +487,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= @@ -551,6 +551,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= @@ -568,8 +570,8 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= -github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= @@ -622,8 +624,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -632,6 +634,8 @@ github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs= github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= @@ -652,6 +656,8 @@ github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOA github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= @@ -678,16 +684,16 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -728,26 +734,27 @@ github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc4 github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -769,6 +776,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= github.com/sanity-io/litter v1.5.1/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ= +github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME= github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -797,8 +806,8 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -833,8 +842,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= -github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= @@ -916,8 +925,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -945,8 +954,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1021,15 +1030,15 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1041,8 +1050,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1104,16 +1113,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1126,8 +1135,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1170,8 +1179,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1216,10 +1225,10 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU= -google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1230,8 +1239,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1243,8 +1252,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/mocks/binary_checker.go b/internal/mocks/binary_checker.go index ec21488b8..6ae2837c5 100644 --- a/internal/mocks/binary_checker.go +++ b/internal/mocks/binary_checker.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks diff --git a/internal/mocks/client.go b/internal/mocks/client.go index 8717a1370..252e51cdc 100644 --- a/internal/mocks/client.go +++ b/internal/mocks/client.go @@ -562,7 +562,7 @@ func (_m *Client) RemoveNode(ctx context.Context, name string) (*rpcpb.RemoveNod } // RemoveSnapshot provides a mock function with given fields: ctx, snapshotName -func (_m *Client) RemoveSnapshot(ctx context.Context, snapshotName string) (*rpcpb.RemoveSnapshotResponse, error) { +func (_m *Client) RemoveSnapshot(ctx context.Context, snapshotName string, opts ...client.OpOption) (*rpcpb.RemoveSnapshotResponse, error) { ret := _m.Called(ctx, snapshotName) if len(ret) == 0 { @@ -689,7 +689,7 @@ func (_m *Client) ResumeNode(ctx context.Context, name string) (*rpcpb.ResumeNod } // SaveSnapshot provides a mock function with given fields: ctx, snapshotName, force -func (_m *Client) SaveSnapshot(ctx context.Context, snapshotName string, force bool) (*rpcpb.SaveSnapshotResponse, error) { +func (_m *Client) SaveSnapshot(ctx context.Context, snapshotName string, force bool, opts ...client.OpOption) (*rpcpb.SaveSnapshotResponse, error) { ret := _m.Called(ctx, snapshotName, force) if len(ret) == 0 { diff --git a/internal/mocks/downloader.go b/internal/mocks/downloader.go index 1a1f45c9a..c554f6de6 100644 --- a/internal/mocks/downloader.go +++ b/internal/mocks/downloader.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks diff --git a/internal/mocks/info.go b/internal/mocks/info.go index efd1b3bdb..ac31cbf9a 100644 --- a/internal/mocks/info.go +++ b/internal/mocks/info.go @@ -358,14 +358,14 @@ func (_m *InfoClient) IsBootstrapped(_a0 context.Context, _a1 string, _a2 ...rpc return r0, r1 } -// Peers provides a mock function with given fields: _a0, _a1 -func (_m *InfoClient) Peers(_a0 context.Context, _a1 ...rpc.Option) ([]info.Peer, error) { - _va := make([]interface{}, len(_a1)) - for _i := range _a1 { - _va[_i] = _a1[_i] +// Peers provides a mock function with given fields: _a0, _a1, _a2 +func (_m *InfoClient) Peers(_a0 context.Context, _a1 []ids.NodeID, _a2 ...rpc.Option) ([]info.Peer, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] } var _ca []interface{} - _ca = append(_ca, _a0) + _ca = append(_ca, _a0, _a1) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -375,19 +375,19 @@ func (_m *InfoClient) Peers(_a0 context.Context, _a1 ...rpc.Option) ([]info.Peer var r0 []info.Peer var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) ([]info.Peer, error)); ok { - return rf(_a0, _a1...) + if rf, ok := ret.Get(0).(func(context.Context, []ids.NodeID, ...rpc.Option) ([]info.Peer, error)); ok { + return rf(_a0, _a1, _a2...) } - if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) []info.Peer); ok { - r0 = rf(_a0, _a1...) + if rf, ok := ret.Get(0).(func(context.Context, []ids.NodeID, ...rpc.Option) []info.Peer); ok { + r0 = rf(_a0, _a1, _a2...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]info.Peer) } } - if rf, ok := ret.Get(1).(func(context.Context, ...rpc.Option) error); ok { - r1 = rf(_a0, _a1...) + if rf, ok := ret.Get(1).(func(context.Context, []ids.NodeID, ...rpc.Option) error); ok { + r1 = rf(_a0, _a1, _a2...) } else { r1 = ret.Error(1) } diff --git a/internal/mocks/installer.go b/internal/mocks/installer.go index 5fa59ad64..02e15e2ba 100644 --- a/internal/mocks/installer.go +++ b/internal/mocks/installer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks diff --git a/internal/mocks/network.go b/internal/mocks/network.go index 56ce44811..d4e8f7446 100644 --- a/internal/mocks/network.go +++ b/internal/mocks/network.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks diff --git a/internal/mocks/pclient.go b/internal/mocks/pclient.go index 070de1d96..0290ae58a 100644 --- a/internal/mocks/pclient.go +++ b/internal/mocks/pclient.go @@ -15,6 +15,8 @@ import ( platformvm "github.com/ava-labs/avalanchego/vms/platformvm" + platformvmapi "github.com/ava-labs/avalanchego/vms/platformvm/api" + rpc "github.com/ava-labs/avalanchego/utils/rpc" secp256k1 "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" @@ -506,6 +508,48 @@ func (_m *PClient) GetHeight(ctx context.Context, options ...rpc.Option) (uint64 return r0, r1 } +// GetL1Validator provides a mock function with given fields: ctx, validationID, options +func (_m *PClient) GetL1Validator(ctx context.Context, validationID ids.ID, options ...rpc.Option) (platformvm.L1Validator, uint64, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, validationID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetL1Validator") + } + + var r0 platformvm.L1Validator + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, ids.ID, ...rpc.Option) (platformvm.L1Validator, uint64, error)); ok { + return rf(ctx, validationID, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, ids.ID, ...rpc.Option) platformvm.L1Validator); ok { + r0 = rf(ctx, validationID, options...) + } else { + r0 = ret.Get(0).(platformvm.L1Validator) + } + + if rf, ok := ret.Get(1).(func(context.Context, ids.ID, ...rpc.Option) uint64); ok { + r1 = rf(ctx, validationID, options...) + } else { + r1 = ret.Get(1).(uint64) + } + + if rf, ok := ret.Get(2).(func(context.Context, ids.ID, ...rpc.Option) error); ok { + r2 = rf(ctx, validationID, options...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // GetMinStake provides a mock function with given fields: ctx, subnetID, options func (_m *PClient) GetMinStake(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, uint64, error) { _va := make([]interface{}, len(options)) @@ -548,6 +592,41 @@ func (_m *PClient) GetMinStake(ctx context.Context, subnetID ids.ID, options ... return r0, r1, r2 } +// GetProposedHeight provides a mock function with given fields: ctx, options +func (_m *PClient) GetProposedHeight(ctx context.Context, options ...rpc.Option) (uint64, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetProposedHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) (uint64, error)); ok { + return rf(ctx, options...) + } + if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) uint64); ok { + r0 = rf(ctx, options...) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, ...rpc.Option) error); ok { + r1 = rf(ctx, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRewardUTXOs provides a mock function with given fields: _a0, _a1, _a2 func (_m *PClient) GetRewardUTXOs(_a0 context.Context, _a1 *api.GetTxArgs, _a2 ...rpc.Option) ([][]byte, error) { _va := make([]interface{}, len(_a2)) @@ -940,7 +1019,7 @@ func (_m *PClient) GetUTXOs(ctx context.Context, addrs []ids.ShortID, limit uint } // GetValidatorsAt provides a mock function with given fields: ctx, subnetID, height, options -func (_m *PClient) GetValidatorsAt(ctx context.Context, subnetID ids.ID, height uint64, options ...rpc.Option) (map[ids.NodeID]*validators.GetValidatorOutput, error) { +func (_m *PClient) GetValidatorsAt(ctx context.Context, subnetID ids.ID, height platformvmapi.Height, options ...rpc.Option) (map[ids.NodeID]*validators.GetValidatorOutput, error) { _va := make([]interface{}, len(options)) for _i := range options { _va[_i] = options[_i] @@ -956,10 +1035,10 @@ func (_m *PClient) GetValidatorsAt(ctx context.Context, subnetID ids.ID, height var r0 map[ids.NodeID]*validators.GetValidatorOutput var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ids.ID, uint64, ...rpc.Option) (map[ids.NodeID]*validators.GetValidatorOutput, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, ids.ID, platformvmapi.Height, ...rpc.Option) (map[ids.NodeID]*validators.GetValidatorOutput, error)); ok { return rf(ctx, subnetID, height, options...) } - if rf, ok := ret.Get(0).(func(context.Context, ids.ID, uint64, ...rpc.Option) map[ids.NodeID]*validators.GetValidatorOutput); ok { + if rf, ok := ret.Get(0).(func(context.Context, ids.ID, platformvmapi.Height, ...rpc.Option) map[ids.NodeID]*validators.GetValidatorOutput); ok { r0 = rf(ctx, subnetID, height, options...) } else { if ret.Get(0) != nil { @@ -967,7 +1046,7 @@ func (_m *PClient) GetValidatorsAt(ctx context.Context, subnetID ids.ID, height } } - if rf, ok := ret.Get(1).(func(context.Context, ids.ID, uint64, ...rpc.Option) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, ids.ID, platformvmapi.Height, ...rpc.Option) error); ok { r1 = rf(ctx, subnetID, height, options...) } else { r1 = ret.Error(1) diff --git a/internal/mocks/plugin_binary_downloader.go b/internal/mocks/plugin_binary_downloader.go index 1972dbd49..20a73b022 100644 --- a/internal/mocks/plugin_binary_downloader.go +++ b/internal/mocks/plugin_binary_downloader.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks diff --git a/internal/mocks/process_checker.go b/internal/mocks/process_checker.go index 2398cd337..06b752ee1 100644 --- a/internal/mocks/process_checker.go +++ b/internal/mocks/process_checker.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks @@ -13,9 +13,9 @@ type ProcessChecker struct { mock.Mock } -// IsServerProcessRunning provides a mock function with given fields: app -func (_m *ProcessChecker) IsServerProcessRunning(app *application.Avalanche) (bool, error) { - ret := _m.Called(app) +// IsServerProcessRunning provides a mock function with given fields: app, prefix +func (_m *ProcessChecker) IsServerProcessRunning(app *application.Avalanche, prefix string) (bool, error) { + ret := _m.Called(app, prefix) if len(ret) == 0 { panic("no return value specified for IsServerProcessRunning") @@ -23,17 +23,17 @@ func (_m *ProcessChecker) IsServerProcessRunning(app *application.Avalanche) (bo var r0 bool var r1 error - if rf, ok := ret.Get(0).(func(*application.Avalanche) (bool, error)); ok { - return rf(app) + if rf, ok := ret.Get(0).(func(*application.Avalanche, string) (bool, error)); ok { + return rf(app, prefix) } - if rf, ok := ret.Get(0).(func(*application.Avalanche) bool); ok { - r0 = rf(app) + if rf, ok := ret.Get(0).(func(*application.Avalanche, string) bool); ok { + r0 = rf(app, prefix) } else { r0 = ret.Get(0).(bool) } - if rf, ok := ret.Get(1).(func(*application.Avalanche) error); ok { - r1 = rf(app) + if rf, ok := ret.Get(1).(func(*application.Avalanche, string) error); ok { + r1 = rf(app, prefix) } else { r1 = ret.Error(1) } diff --git a/internal/mocks/prompter.go b/internal/mocks/prompter.go index ce4923a2d..6281cbc90 100644 --- a/internal/mocks/prompter.go +++ b/internal/mocks/prompter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.26.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks @@ -28,6 +28,10 @@ type Prompter struct { func (_m *Prompter) CaptureAddress(promptStr string) (common.Address, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureAddress") + } + var r0 common.Address var r1 error if rf, ok := ret.Get(0).(func(string) (common.Address, error)); ok { @@ -54,6 +58,10 @@ func (_m *Prompter) CaptureAddress(promptStr string) (common.Address, error) { func (_m *Prompter) CaptureAddresses(promptStr string) ([]common.Address, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureAddresses") + } + var r0 []common.Address var r1 error if rf, ok := ret.Get(0).(func(string) ([]common.Address, error)); ok { @@ -80,6 +88,10 @@ func (_m *Prompter) CaptureAddresses(promptStr string) ([]common.Address, error) func (_m *Prompter) CaptureDate(promptStr string) (time.Time, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureDate") + } + var r0 time.Time var r1 error if rf, ok := ret.Get(0).(func(string) (time.Time, error)); ok { @@ -104,6 +116,10 @@ func (_m *Prompter) CaptureDate(promptStr string) (time.Time, error) { func (_m *Prompter) CaptureEmail(promptStr string) (string, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureEmail") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string) (string, error)); ok { @@ -128,6 +144,10 @@ func (_m *Prompter) CaptureEmail(promptStr string) (string, error) { func (_m *Prompter) CaptureExistingFilepath(promptStr string) (string, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureExistingFilepath") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string) (string, error)); ok { @@ -152,6 +172,10 @@ func (_m *Prompter) CaptureExistingFilepath(promptStr string) (string, error) { func (_m *Prompter) CaptureFloat(promptStr string, validator func(float64) error) (float64, error) { ret := _m.Called(promptStr, validator) + if len(ret) == 0 { + panic("no return value specified for CaptureFloat") + } + var r0 float64 var r1 error if rf, ok := ret.Get(0).(func(string, func(float64) error) (float64, error)); ok { @@ -176,6 +200,10 @@ func (_m *Prompter) CaptureFloat(promptStr string, validator func(float64) error func (_m *Prompter) CaptureFujiDuration(promptStr string) (time.Duration, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureFujiDuration") + } + var r0 time.Duration var r1 error if rf, ok := ret.Get(0).(func(string) (time.Duration, error)); ok { @@ -200,6 +228,10 @@ func (_m *Prompter) CaptureFujiDuration(promptStr string) (time.Duration, error) func (_m *Prompter) CaptureFutureDate(promptStr string, minDate time.Time) (time.Time, error) { ret := _m.Called(promptStr, minDate) + if len(ret) == 0 { + panic("no return value specified for CaptureFutureDate") + } + var r0 time.Time var r1 error if rf, ok := ret.Get(0).(func(string, time.Time) (time.Time, error)); ok { @@ -224,6 +256,10 @@ func (_m *Prompter) CaptureFutureDate(promptStr string, minDate time.Time) (time func (_m *Prompter) CaptureGitURL(promptStr string) (*url.URL, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureGitURL") + } + var r0 *url.URL var r1 error if rf, ok := ret.Get(0).(func(string) (*url.URL, error)); ok { @@ -250,6 +286,10 @@ func (_m *Prompter) CaptureGitURL(promptStr string) (*url.URL, error) { func (_m *Prompter) CaptureID(promptStr string) (ids.ID, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureID") + } + var r0 ids.ID var r1 error if rf, ok := ret.Get(0).(func(string) (ids.ID, error)); ok { @@ -273,21 +313,25 @@ func (_m *Prompter) CaptureID(promptStr string) (ids.ID, error) { } // CaptureIndex provides a mock function with given fields: promptStr, options -func (_m *Prompter) CaptureIndex(promptStr string, options []interface{}) (int, error) { +func (_m *Prompter) CaptureIndex(promptStr string, options []any) (int, error) { ret := _m.Called(promptStr, options) + if len(ret) == 0 { + panic("no return value specified for CaptureIndex") + } + var r0 int var r1 error - if rf, ok := ret.Get(0).(func(string, []interface{}) (int, error)); ok { + if rf, ok := ret.Get(0).(func(string, []any) (int, error)); ok { return rf(promptStr, options) } - if rf, ok := ret.Get(0).(func(string, []interface{}) int); ok { + if rf, ok := ret.Get(0).(func(string, []any) int); ok { r0 = rf(promptStr, options) } else { r0 = ret.Get(0).(int) } - if rf, ok := ret.Get(1).(func(string, []interface{}) error); ok { + if rf, ok := ret.Get(1).(func(string, []any) error); ok { r1 = rf(promptStr, options) } else { r1 = ret.Error(1) @@ -296,23 +340,27 @@ func (_m *Prompter) CaptureIndex(promptStr string, options []interface{}) (int, return r0, r1 } -// CaptureInt provides a mock function with given fields: promptStr -func (_m *Prompter) CaptureInt(promptStr string) (int, error) { - ret := _m.Called(promptStr) +// CaptureInt provides a mock function with given fields: promptStr, validator +func (_m *Prompter) CaptureInt(promptStr string, validator func(int) error) (int, error) { + ret := _m.Called(promptStr, validator) + + if len(ret) == 0 { + panic("no return value specified for CaptureInt") + } var r0 int var r1 error - if rf, ok := ret.Get(0).(func(string) (int, error)); ok { - return rf(promptStr) + if rf, ok := ret.Get(0).(func(string, func(int) error) (int, error)); ok { + return rf(promptStr, validator) } - if rf, ok := ret.Get(0).(func(string) int); ok { - r0 = rf(promptStr) + if rf, ok := ret.Get(0).(func(string, func(int) error) int); ok { + r0 = rf(promptStr, validator) } else { r0 = ret.Get(0).(int) } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(promptStr) + if rf, ok := ret.Get(1).(func(string, func(int) error) error); ok { + r1 = rf(promptStr, validator) } else { r1 = ret.Error(1) } @@ -324,6 +372,10 @@ func (_m *Prompter) CaptureInt(promptStr string) (int, error) { func (_m *Prompter) CaptureList(promptStr string, options []string) (string, error) { ret := _m.Called(promptStr, options) + if len(ret) == 0 { + panic("no return value specified for CaptureList") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, []string) (string, error)); ok { @@ -348,6 +400,10 @@ func (_m *Prompter) CaptureList(promptStr string, options []string) (string, err func (_m *Prompter) CaptureListWithSize(promptStr string, options []string, size int) (string, error) { ret := _m.Called(promptStr, options, size) + if len(ret) == 0 { + panic("no return value specified for CaptureListWithSize") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, []string, int) (string, error)); ok { @@ -372,6 +428,10 @@ func (_m *Prompter) CaptureListWithSize(promptStr string, options []string, size func (_m *Prompter) CaptureMainnetDuration(promptStr string) (time.Duration, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureMainnetDuration") + } + var r0 time.Duration var r1 error if rf, ok := ret.Get(0).(func(string) (time.Duration, error)); ok { @@ -392,10 +452,22 @@ func (_m *Prompter) CaptureMainnetDuration(promptStr string) (time.Duration, err return r0, r1 } +func (_m *Prompter) CaptureEtnaDuration(promptStr string) (time.Duration, error) { + return _m.CaptureMainnetDuration(promptStr) +} + +func (_m *Prompter) CaptureDuration(promptStr string) (time.Duration, error) { + return _m.CaptureMainnetDuration(promptStr) +} + // CaptureNewFilepath provides a mock function with given fields: promptStr func (_m *Prompter) CaptureNewFilepath(promptStr string) (string, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureNewFilepath") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string) (string, error)); ok { @@ -420,6 +492,10 @@ func (_m *Prompter) CaptureNewFilepath(promptStr string) (string, error) { func (_m *Prompter) CaptureNoYes(promptStr string) (bool, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureNoYes") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { @@ -444,6 +520,10 @@ func (_m *Prompter) CaptureNoYes(promptStr string) (bool, error) { func (_m *Prompter) CaptureNodeID(promptStr string) (ids.NodeID, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureNodeID") + } + var r0 ids.NodeID var r1 error if rf, ok := ret.Get(0).(func(string) (ids.NodeID, error)); ok { @@ -470,6 +550,10 @@ func (_m *Prompter) CaptureNodeID(promptStr string) (ids.NodeID, error) { func (_m *Prompter) CapturePChainAddress(promptStr string, network models.Network) (string, error) { ret := _m.Called(promptStr, network) + if len(ret) == 0 { + panic("no return value specified for CapturePChainAddress") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, models.Network) (string, error)); ok { @@ -494,6 +578,10 @@ func (_m *Prompter) CapturePChainAddress(promptStr string, network models.Networ func (_m *Prompter) CapturePositiveBigInt(promptStr string) (*big.Int, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CapturePositiveBigInt") + } + var r0 *big.Int var r1 error if rf, ok := ret.Get(0).(func(string) (*big.Int, error)); ok { @@ -520,6 +608,10 @@ func (_m *Prompter) CapturePositiveBigInt(promptStr string) (*big.Int, error) { func (_m *Prompter) CapturePositiveInt(promptStr string, comparators []prompts.Comparator) (int, error) { ret := _m.Called(promptStr, comparators) + if len(ret) == 0 { + panic("no return value specified for CapturePositiveInt") + } + var r0 int var r1 error if rf, ok := ret.Get(0).(func(string, []prompts.Comparator) (int, error)); ok { @@ -544,6 +636,10 @@ func (_m *Prompter) CapturePositiveInt(promptStr string, comparators []prompts.C func (_m *Prompter) CaptureRepoBranch(promptStr string, repo string) (string, error) { ret := _m.Called(promptStr, repo) + if len(ret) == 0 { + panic("no return value specified for CaptureRepoBranch") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok { @@ -568,6 +664,10 @@ func (_m *Prompter) CaptureRepoBranch(promptStr string, repo string) (string, er func (_m *Prompter) CaptureRepoFile(promptStr string, repo string, branch string) (string, error) { ret := _m.Called(promptStr, repo, branch) + if len(ret) == 0 { + panic("no return value specified for CaptureRepoFile") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, string, string) (string, error)); ok { @@ -592,6 +692,10 @@ func (_m *Prompter) CaptureRepoFile(promptStr string, repo string, branch string func (_m *Prompter) CaptureString(promptStr string) (string, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureString") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string) (string, error)); ok { @@ -616,6 +720,10 @@ func (_m *Prompter) CaptureString(promptStr string) (string, error) { func (_m *Prompter) CaptureStringAllowEmpty(promptStr string) (string, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureStringAllowEmpty") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string) (string, error)); ok { @@ -640,6 +748,10 @@ func (_m *Prompter) CaptureStringAllowEmpty(promptStr string) (string, error) { func (_m *Prompter) CaptureURL(promptStr string, validateConnection bool) (string, error) { ret := _m.Called(promptStr, validateConnection) + if len(ret) == 0 { + panic("no return value specified for CaptureURL") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, bool) (string, error)); ok { @@ -660,10 +772,42 @@ func (_m *Prompter) CaptureURL(promptStr string, validateConnection bool) (strin return r0, r1 } +// CaptureUint16 provides a mock function with given fields: promptStr +func (_m *Prompter) CaptureUint16(promptStr string) (uint16, error) { + ret := _m.Called(promptStr) + + if len(ret) == 0 { + panic("no return value specified for CaptureUint16") + } + + var r0 uint16 + var r1 error + if rf, ok := ret.Get(0).(func(string) (uint16, error)); ok { + return rf(promptStr) + } + if rf, ok := ret.Get(0).(func(string) uint16); ok { + r0 = rf(promptStr) + } else { + r0 = ret.Get(0).(uint16) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(promptStr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CaptureUint32 provides a mock function with given fields: promptStr func (_m *Prompter) CaptureUint32(promptStr string) (uint32, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureUint32") + } + var r0 uint32 var r1 error if rf, ok := ret.Get(0).(func(string) (uint32, error)); ok { @@ -688,6 +832,10 @@ func (_m *Prompter) CaptureUint32(promptStr string) (uint32, error) { func (_m *Prompter) CaptureUint64(promptStr string) (uint64, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureUint64") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(string) (uint64, error)); ok { @@ -712,6 +860,10 @@ func (_m *Prompter) CaptureUint64(promptStr string) (uint64, error) { func (_m *Prompter) CaptureUint64Compare(promptStr string, comparators []prompts.Comparator) (uint64, error) { ret := _m.Called(promptStr, comparators) + if len(ret) == 0 { + panic("no return value specified for CaptureUint64Compare") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(string, []prompts.Comparator) (uint64, error)); ok { @@ -732,10 +884,42 @@ func (_m *Prompter) CaptureUint64Compare(promptStr string, comparators []prompts return r0, r1 } +// CaptureUint8 provides a mock function with given fields: promptStr +func (_m *Prompter) CaptureUint8(promptStr string) (uint8, error) { + ret := _m.Called(promptStr) + + if len(ret) == 0 { + panic("no return value specified for CaptureUint8") + } + + var r0 uint8 + var r1 error + if rf, ok := ret.Get(0).(func(string) (uint8, error)); ok { + return rf(promptStr) + } + if rf, ok := ret.Get(0).(func(string) uint8); ok { + r0 = rf(promptStr) + } else { + r0 = ret.Get(0).(uint8) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(promptStr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CaptureValidatedString provides a mock function with given fields: promptStr, validator func (_m *Prompter) CaptureValidatedString(promptStr string, validator func(string) error) (string, error) { ret := _m.Called(promptStr, validator) + if len(ret) == 0 { + panic("no return value specified for CaptureValidatedString") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, func(string) error) (string, error)); ok { @@ -756,10 +940,42 @@ func (_m *Prompter) CaptureValidatedString(promptStr string, validator func(stri return r0, r1 } +// CaptureValidatorBalance provides a mock function with given fields: promptStr, availableBalance +func (_m *Prompter) CaptureValidatorBalance(promptStr string, availableBalance uint64) (uint64, error) { + ret := _m.Called(promptStr, availableBalance) + + if len(ret) == 0 { + panic("no return value specified for CaptureValidatorBalance") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(string, uint64) (uint64, error)); ok { + return rf(promptStr, availableBalance) + } + if rf, ok := ret.Get(0).(func(string, uint64) uint64); ok { + r0 = rf(promptStr, availableBalance) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(string, uint64) error); ok { + r1 = rf(promptStr, availableBalance) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CaptureVersion provides a mock function with given fields: promptStr func (_m *Prompter) CaptureVersion(promptStr string) (string, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureVersion") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string) (string, error)); ok { @@ -784,6 +1000,10 @@ func (_m *Prompter) CaptureVersion(promptStr string) (string, error) { func (_m *Prompter) CaptureWeight(promptStr string) (uint64, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureWeight") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(string) (uint64, error)); ok { @@ -808,6 +1028,10 @@ func (_m *Prompter) CaptureWeight(promptStr string) (uint64, error) { func (_m *Prompter) CaptureXChainAddress(promptStr string, network models.Network) (string, error) { ret := _m.Called(promptStr, network) + if len(ret) == 0 { + panic("no return value specified for CaptureXChainAddress") + } + var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, models.Network) (string, error)); ok { @@ -832,6 +1056,10 @@ func (_m *Prompter) CaptureXChainAddress(promptStr string, network models.Networ func (_m *Prompter) CaptureYesNo(promptStr string) (bool, error) { ret := _m.Called(promptStr) + if len(ret) == 0 { + panic("no return value specified for CaptureYesNo") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { @@ -856,6 +1084,10 @@ func (_m *Prompter) CaptureYesNo(promptStr string) (bool, error) { func (_m *Prompter) ChooseKeyOrLedger(goal string) (bool, error) { ret := _m.Called(goal) + if len(ret) == 0 { + panic("no return value specified for ChooseKeyOrLedger") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { @@ -876,13 +1108,12 @@ func (_m *Prompter) ChooseKeyOrLedger(goal string) (bool, error) { return r0, r1 } -type mockConstructorTestingTNewPrompter interface { +// NewPrompter creates a new instance of Prompter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPrompter(t interface { mock.TestingT Cleanup(func()) -} - -// NewPrompter creates a new instance of Prompter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewPrompter(t mockConstructorTestingTNewPrompter) *Prompter { +}) *Prompter { mock := &Prompter{} mock.Mock.Test(t) diff --git a/internal/mocks/publisher.go b/internal/mocks/publisher.go index 45d6028a6..9b4a6d866 100644 --- a/internal/mocks/publisher.go +++ b/internal/mocks/publisher.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks diff --git a/pkg/application/app.go b/pkg/application/app.go index 8b3221d7c..ee021e523 100644 --- a/pkg/application/app.go +++ b/pkg/application/app.go @@ -46,8 +46,8 @@ func (app *Avalanche) Setup(baseDir string, log logging.Logger, conf *config.Con app.Downloader = downloader } -func (app *Avalanche) GetRunFile() string { - return filepath.Join(app.GetRunDir(), constants.ServerRunFile) +func (app *Avalanche) GetRunFile(prefix string) string { + return filepath.Join(app.GetRunDir(), prefix+constants.ServerRunFile) } func (app *Avalanche) GetSnapshotsDir() string { @@ -89,6 +89,10 @@ func (app *Avalanche) GetPluginsDir() string { return filepath.Join(app.baseDir, constants.PluginDir) } +func (app *Avalanche) GetLocalDir(clusterName string) string { + return filepath.Join(app.baseDir, constants.LocalDir, clusterName) +} + // Remove all plugins from plugin dir func (app *Avalanche) ResetPluginsDir() error { pluginDir := app.GetPluginsDir() @@ -160,7 +164,7 @@ func (app *Avalanche) GetSubnetEVMBinDir() string { } func (app *Avalanche) GetUpgradeBytesFilepath(blockchainName string) string { - return filepath.Join(app.GetSubnetDir(), blockchainName, constants.UpgradeBytesFileName) + return filepath.Join(app.GetSubnetDir(), blockchainName, constants.UpgradeFileName) } func (app *Avalanche) GetCustomVMPath(blockchainName string) string { @@ -286,7 +290,7 @@ func (app *Avalanche) GetKey(keyName string, network models.Network, createIfMis } func (app *Avalanche) GetUpgradeBytesFilePath(blockchainName string) string { - return filepath.Join(app.GetSubnetDir(), blockchainName, constants.UpgradeBytesFileName) + return filepath.Join(app.GetSubnetDir(), blockchainName, constants.UpgradeFileName) } func (app *Avalanche) GetDownloader() Downloader { @@ -533,6 +537,8 @@ func (app *Avalanche) UpdateSidecarNetworks( blockchainID ids.ID, teleporterMessengerAddress string, teleporterRegistryAddress string, + bootstrapValidators []models.SubnetValidator, + clusterName string, ) error { if sc.Networks == nil { sc.Networks = make(map[string]models.NetworkData) @@ -543,6 +549,8 @@ func (app *Avalanche) UpdateSidecarNetworks( RPCVersion: sc.RPCVersion, TeleporterMessengerAddress: teleporterMessengerAddress, TeleporterRegistryAddress: teleporterRegistryAddress, + BootstrapValidators: bootstrapValidators, + ClusterName: clusterName, } if err := app.UpdateSidecar(sc); err != nil { return fmt.Errorf("creation of chains and subnet was successful, but failed to update sidecar: %w", err) @@ -657,6 +665,11 @@ func (app *Avalanche) LoadClusterNodeConfig(nodeName string) (models.NodeConfig, func (app *Avalanche) LoadClustersConfig() (models.ClustersConfig, error) { clustersConfigPath := app.GetClustersConfigPath() + if !utils.FileExists(clustersConfigPath) { + return models.ClustersConfig{ + Clusters: map[string]models.ClusterConfig{}, + }, nil + } jsonBytes, err := os.ReadFile(clustersConfigPath) if err != nil { return models.ClustersConfig{}, err @@ -694,6 +707,13 @@ func (app *Avalanche) LoadClustersConfig() (models.ClustersConfig, error) { return models.ClustersConfig{}, fmt.Errorf("unsupported clusters config version %s", v) } +func (app *Avalanche) GetClustersConfig() (models.ClustersConfig, error) { + if app.ClustersConfigExists() { + return app.LoadClustersConfig() + } + return models.ClustersConfig{}, nil +} + func (app *Avalanche) WriteClustersConfigFile(clustersConfig *models.ClustersConfig) error { clustersConfigPath := app.GetClustersConfigPath() if err := os.MkdirAll(filepath.Dir(clustersConfigPath), constants.DefaultPerms755); err != nil { @@ -792,25 +812,19 @@ func (app *Avalanche) SetupMonitoringEnv() error { } func (app *Avalanche) ClusterExists(clusterName string) (bool, error) { - clustersConfig := models.ClustersConfig{} - if app.ClustersConfigExists() { - var err error - clustersConfig, err = app.LoadClustersConfig() - if err != nil { - return false, err - } + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return false, err } _, ok := clustersConfig.Clusters[clusterName] return ok, nil } func (app *Avalanche) GetClusterConfig(clusterName string) (models.ClusterConfig, error) { - exists, err := app.ClusterExists(clusterName) - if err != nil { + if exists, err := app.ClusterExists(clusterName); err != nil { return models.ClusterConfig{}, err - } - if !exists { - return models.ClusterConfig{}, fmt.Errorf("cluster %q does not exists", clusterName) + } else if !exists { + return models.ClusterConfig{}, fmt.Errorf("cluster does not exists") } clustersConfig, err := app.LoadClustersConfig() if err != nil { diff --git a/pkg/binutils/constants.go b/pkg/binutils/constants.go index 8c8cdb4cd..9116a8d52 100644 --- a/pkg/binutils/constants.go +++ b/pkg/binutils/constants.go @@ -6,12 +6,17 @@ import "time" const ( gRPCClientLogLevel = "error" - gRPCServerPort = ":8097" - gRPCGatewayPort = ":8098" - gRPCServerEndpoint = "localhost" + gRPCServerPort gRPCDialTimeout = 10 * time.Second avalanchegoBinPrefix = "avalanchego-" subnetEVMBinPrefix = "subnet-evm-" maxCopy = 2147483648 // 2 GB + + LocalNetworkGRPCServerPort = ":8097" + LocalNetworkGRPCGatewayPort = ":8098" + LocalNetworkGRPCServerEndpoint = "localhost" + LocalNetworkGRPCServerPort + + LocalClusterGRPCServerPort = ":8090" + LocalClusterGRPCGatewayPort = ":8091" + LocalClusterGRPCServerEndpoint = "localhost" + LocalClusterGRPCServerPort ) diff --git a/pkg/binutils/processes.go b/pkg/binutils/processes.go index 71ee77e4e..0abea5b54 100644 --- a/pkg/binutils/processes.go +++ b/pkg/binutils/processes.go @@ -35,7 +35,7 @@ var ErrGRPCTimeout = errors.New("timed out trying to contact backend controller, type ProcessChecker interface { // IsServerProcessRunning returns true if the gRPC server is running, // or false if not - IsServerProcessRunning(app *application.Avalanche) (bool, error) + IsServerProcessRunning(app *application.Avalanche, prefix string) (bool, error) } type realProcessRunner struct{} @@ -72,6 +72,13 @@ func WithDialTimeout(dialTimeout time.Duration) GRPCClientOpOption { // NewGRPCClient hides away the details (params) of creating a gRPC server connection func NewGRPCClient(opts ...GRPCClientOpOption) (client.Client, error) { + return NewGRPCClientWithEndpoint(LocalNetworkGRPCServerEndpoint, opts...) +} + +func NewGRPCClientWithEndpoint( + serverEndpoint string, + opts ...GRPCClientOpOption, +) (client.Client, error) { op := GRPCClientOp{ dialTimeout: gRPCDialTimeout, } @@ -89,7 +96,7 @@ func NewGRPCClient(opts ...GRPCClientOpOption) (client.Client, error) { return nil, err } client, err := client.New(client.Config{ - Endpoint: gRPCServerEndpoint, + Endpoint: serverEndpoint, DialTimeout: op.dialTimeout, }, log) if errors.Is(err, context.DeadlineExceeded) { @@ -116,7 +123,11 @@ func NewGRPCClient(opts ...GRPCClientOpOption) (client.Client, error) { } // NewGRPCServer hides away the details (params) of creating a gRPC server -func NewGRPCServer(snapshotsDir string) (server.Server, error) { +func NewGRPCServer( + serverPort string, + gatewayPort string, + snapshotsDir string, +) (server.Server, error) { logFactory := logging.NewFactory(logging.Config{ DisplayLevel: logging.Info, LogLevel: logging.Off, @@ -126,8 +137,8 @@ func NewGRPCServer(snapshotsDir string) (server.Server, error) { return nil, err } return server.New(server.Config{ - Port: gRPCServerPort, - GwPort: gRPCGatewayPort, + Port: serverPort, + GwPort: gatewayPort, DialTimeout: gRPCDialTimeout, SnapshotsDir: snapshotsDir, RedirectNodesOutput: false, @@ -137,8 +148,11 @@ func NewGRPCServer(snapshotsDir string) (server.Server, error) { // IsServerProcessRunning returns true if the gRPC server is running, // or false if not -func (*realProcessRunner) IsServerProcessRunning(app *application.Avalanche) (bool, error) { - pid, err := GetServerPID(app) +func (*realProcessRunner) IsServerProcessRunning( + app *application.Avalanche, + prefix string, +) (bool, error) { + pid, err := GetServerPID(app, prefix) if err != nil { if !errors.Is(err, os.ErrNotExist) { return false, err @@ -167,9 +181,12 @@ type runFile struct { GRPCserverFileName string `json:"gRPCserverFileName"` } -func GetBackendLogFile(app *application.Avalanche) (string, error) { +func GetBackendLogFile( + app *application.Avalanche, + prefix string, +) (string, error) { var rf runFile - serverRunFilePath := app.GetRunFile() + serverRunFilePath := app.GetRunFile(prefix) run, err := os.ReadFile(serverRunFilePath) if err != nil { return "", fmt.Errorf("failed reading process info file at %s: %w", serverRunFilePath, err) @@ -177,13 +194,15 @@ func GetBackendLogFile(app *application.Avalanche) (string, error) { if err := json.Unmarshal(run, &rf); err != nil { return "", fmt.Errorf("failed unmarshalling server run file at %s: %w", serverRunFilePath, err) } - return rf.GRPCserverFileName, nil } -func GetServerPID(app *application.Avalanche) (int, error) { +func GetServerPID( + app *application.Avalanche, + prefix string, +) (int, error) { var rf runFile - serverRunFilePath := app.GetRunFile() + serverRunFilePath := app.GetRunFile(prefix) run, err := os.ReadFile(serverRunFilePath) if err != nil { return 0, fmt.Errorf("failed reading process info file at %s: %w", serverRunFilePath, err) @@ -200,19 +219,37 @@ func GetServerPID(app *application.Avalanche) (int, error) { // StartServerProcess starts the gRPC server as a reentrant process of this binary // it just executes `avalanche-cli backend start` -func StartServerProcess(app *application.Avalanche) error { +func StartServerProcess( + app *application.Avalanche, + prefix string, + serverPort string, + gatewayPort string, + snapshotsDir string, + logPath string, +) error { thisBin := reexec.Self() - args := []string{constants.BackendCmd} + args := []string{ + constants.BackendCmd, + "--server-port", + serverPort, + "--gateway-port", + gatewayPort, + "--snapshots-dir", + snapshotsDir, + } cmd := exec.Command(thisBin, args...) - outputDirPrefix := path.Join(app.GetRunDir(), "server") - outputDir, err := anrutils.MkDirWithTimestamp(outputDirPrefix) - if err != nil { - return err + if logPath == "" { + outputDirPrefix := path.Join(app.GetRunDir(), prefix+"server") + outputDir, err := anrutils.MkDirWithTimestamp(outputDirPrefix) + if err != nil { + return err + } + logPath = path.Join(outputDir, "avalanche-cli-backend.log") } - outputFile, err := os.Create(path.Join(outputDir, "avalanche-cli-backend.log")) + outputFile, err := os.Create(logPath) if err != nil { return err } @@ -236,14 +273,19 @@ func StartServerProcess(app *application.Avalanche) error { return err } - if err := os.WriteFile(app.GetRunFile(), rfBytes, perms.ReadWrite); err != nil { + if err := os.WriteFile(app.GetRunFile(prefix), rfBytes, perms.ReadWrite); err != nil { app.Log.Warn("could not write gRPC process info to file", zap.Error(err)) } return nil } -func KillgRPCServerProcess(app *application.Avalanche) error { - cli, err := NewGRPCClient( +func KillgRPCServerProcess( + app *application.Avalanche, + serverEndpoint string, + prefix string, +) error { + cli, err := NewGRPCClientWithEndpoint( + serverEndpoint, WithAvoidRPCVersionCheck(true), WithDialTimeout(constants.FastGRPCDialTimeout), ) @@ -262,7 +304,7 @@ func KillgRPCServerProcess(app *application.Avalanche) error { } } - pid, err := GetServerPID(app) + pid, err := GetServerPID(app, prefix) if err != nil { return fmt.Errorf("failed getting PID from run file: %w", err) } @@ -274,7 +316,7 @@ func KillgRPCServerProcess(app *application.Avalanche) error { return fmt.Errorf("failed killing process with pid %d: %w", pid, err) } - serverRunFilePath := app.GetRunFile() + serverRunFilePath := app.GetRunFile(prefix) if err := os.Remove(serverRunFilePath); err != nil { return fmt.Errorf("failed removing run file %s: %w", serverRunFilePath, err) } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 904daecd6..977bf6df2 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -18,7 +18,10 @@ const ( BaseDirName = ".avalanche-cli" LogDir = "logs" - ServerRunFile = "gRPCserver.run" + ServerRunFile = "gRPCserver.run" + ServerRunFileLocalNetworkPrefix = "" + ServerRunFileLocalClusterPrefix = "localcluster_" + AvalancheCliBinDir = "bin" RunDir = "runs" ServicesDir = "services" @@ -26,6 +29,7 @@ const ( SuffixSeparator = "_" SidecarFileName = "sidecar.json" GenesisFileName = "genesis.json" + UpgradeFileName = "upgrade.json" AliasesFileName = "aliases.json" SidecarSuffix = SuffixSeparator + SidecarFileName GenesisSuffix = SuffixSeparator + GenesisFileName @@ -51,10 +55,12 @@ const ( CloudOperationTimeout = 2 * time.Minute ANRRequestTimeout = 3 * time.Minute - APIRequestTimeout = 30 * time.Second - APIRequestLargeTimeout = 2 * time.Minute + APIRequestTimeout = 5 * time.Second + APIRequestLargeTimeout = 5 * time.Second FastGRPCDialTimeout = 100 * time.Millisecond + FujiBootstrapTimeout = 5 * time.Minute + SSHServerStartTimeout = 1 * time.Minute SSHScriptTimeout = 2 * time.Minute SSHLongRunningScriptTimeout = 10 * time.Minute @@ -69,8 +75,8 @@ const ( AWSGP3DefaultIOPS = 3000 AWSGP3DefaultThroughput = 125 SimulatePublicNetwork = "SIMULATE_PUBLIC_NETWORK" - - OperateOfflineEnvVarName = "CLIOFFLINE" + LatestAvalancheGoVersion = "latest" + OperateOfflineEnvVarName = "CLIOFFLINE" PublicAccess HTTPAccess = true PrivateAccess HTTPAccess = false @@ -162,12 +168,20 @@ const ( Disable = "disable" - TimeParseLayout = "2006-01-02 15:04:05" - MinStakeWeight = 1 - DefaultStakeWeight = 20 - AVAXSymbol = "AVAX" - DefaultFujiStakeDuration = "48h" - DefaultMainnetStakeDuration = "336h" + TimeParseLayout = "2006-01-02 15:04:05" + MinStakeWeight = 1 + // Default balance when we prompt users for bootstrap validators + // nAVAX + BootstrapValidatorBalance = 1000000000 + // Default weight when we prompt users for bootstrap validators + BootstrapValidatorWeight = 100 + // Default weight when we prompt users for non bootstrap validators + NonBootstrapValidatorWeight = BootstrapValidatorWeight / 5 + DefaultStakeWeight = 20 + AVAXSymbol = "AVAX" + DefaultFujiStakeDuration = "48h" + DefaultMainnetStakeDuration = "336h" + DefaultValidationIDExpiryDuration = 24 * time.Hour // The absolute minimum is 25 seconds, but set to 1 minute to allow for // time to go through the command DevnetStakingStartLeadTime = 30 * time.Second @@ -280,6 +294,8 @@ const ( DevnetLocalAWMRelayerMetricsPort = 9092 FujiLocalAWMRelayerMetricsPort = 9093 + DevnetFlagsProposerVMUseCurrentHeight = true + SubnetEVMBin = "subnet-evm" DefaultNodeRunURL = "http://127.0.0.1:9650" @@ -334,7 +350,6 @@ const ( AvalancheGoDockerImage = "avaplatform/avalanchego" AvalancheGoGitRepo = "https://github.com/ava-labs/avalanchego" - UpgradeBytesFileName = "upgrade.json" UpgradeBytesLockExtension = ".lock" NotAvailableLabel = "Not available" BackendCmd = "avalanche-cli-backend" @@ -351,6 +366,7 @@ const ( BlockchainIDLabel = "BlockchainID: " PluginDir = "plugins" + LocalDir = "local" MetricsNetwork = "network" MultiSig = "multi-sig" diff --git a/pkg/constants/etna.go b/pkg/constants/etna.go new file mode 100644 index 000000000..a5d073da2 --- /dev/null +++ b/pkg/constants/etna.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package constants + +import ( + _ "embed" + "time" +) + +//go:embed etnaDevnet/genesis.json +var EtnaDevnetGenesisData []byte + +//go:embed etnaDevnet/upgrade.json +var EtnaDevnetUpgradeData []byte + +const ( + EtnaDevnetEndpoint = "https://etna.avax-dev.network" + EtnaDevnetNetworkID = uint32(76) +) + +var ( + EtnaDevnetBootstrapNodeIDs = []string{ + "NodeID-WrLWMK5sJ4dBUAsx1dP2FUyTqrYwbFA1", + "NodeID-bojBKDrpt81bYhxYKQfLw89V7CpoH2m7", + "NodeID-8LbTmmGsDC991SbD8Nkx88VULT3XYzYXC", + "NodeID-DDhXtFm6Q9tCq2yiFRmcSMKvHgUgh8yQC", + "NodeID-QDYnWDQd6g4cQ5H6yiWNqSmfRMBqEH9AG", + } + EtnaDevnetBootstrapIPs = []string{ + "107.21.11.213:9651", + "34.233.248.130:9651", + "52.201.126.172:9651", + "35.170.144.5:9651", + "98.82.41.186:9651", + } +) + +const StakingEtnaMinimumDuration = 100 * time.Second diff --git a/pkg/constants/etnaDevnet/genesis.json b/pkg/constants/etnaDevnet/genesis.json new file mode 100644 index 000000000..38cd96de0 --- /dev/null +++ b/pkg/constants/etnaDevnet/genesis.json @@ -0,0 +1,87 @@ +{ + "networkID": 76, + "allocations": [ + { + "ethAddr": "0xC71A61a815e49d16C425482A342a367CD42E38a6", + "avaxAddr": "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9", + "initialAmount": 500000000000000000, + "unlockSchedule": [ + { + "amount": 100000000000000000, + "locktime": 1633824000 + }, + { + "amount": 100000000000000000, + "locktime": 1633825000 + }, + { + "amount": 100000000000000000, + "locktime": 1633826000 + }, + { + "amount": 100000000000000000, + "locktime": 1633827000 + }, + { + "amount": 100000000000000000, + "locktime": 1633828000 + } + ] + } + ], + "startTime": 1725300000, + "initialStakeDuration": 31530000, + "initialStakeDurationOffset": 5400, + "initialStakedFunds": [ + "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9" + ], + "initialStakers": [ + { + "nodeID": "NodeID-gpXWBExQSZXqJPQt6L6MnveUfgr7HJ4q", + "rewardAddress": "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9", + "delegationFee": 62500, + "signer": { + "publicKey": "0xa14d67f097d7e6514696fd83080794b6b5ca664001f2ede4fd6f01da4933db875ff028fec42b29fc5524941e0f20830f", + "proofOfPossession": "0x82352ae1e01038173e92e088bd34c2bee9cc4b34df5c5e8bd2773cef9238eef870c2f93fa89607332ccbb84aa66608c706b7c2f127b8b80c461c04bbc6082afbffe220aac79f66533ea7c63f0451d7d32456369dd335c9710938ee4111d08d79" + } + }, + { + "nodeID": "NodeID-78ibWpjtZz5ZGT6EyTEdu8VKmboUHTuGT", + "rewardAddress": "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9", + "delegationFee": 62500, + "signer": { + "publicKey": "0x8327dbe1ba411c270637b080a8471fb41eeb8a9b3917af0727501ef8bdaa901d063780bd702f30f458a61f3d4297dc98", + "proofOfPossession": "0xa9c039b5765ab068bd632bbcdc9bc2a53f29e2c56b33e30d732a23d8c430d53ef47ebccfaa5cfcedd8f041c2c1348f0b0eac413192b7544d284f82d1fa0f74f98d5805905363b8186efdef6e77182fb1e7147f8511e900d195db06da6a22f0a0" + } + }, + { + "nodeID": "NodeID-L4CY8B5uVSDe4cnN1BpeDsHacMp4q4q8q", + "rewardAddress": "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9", + "delegationFee": 62500, + "signer": { + "publicKey": "0xa98c646a8c862ec15326e4cfe2a0f66a8fb7cf555765f83ff320a1a76268228f3c8b262d1de4008e0ba49a89cfabfb95", + "proofOfPossession": "0x95c816a4d292a47c4d9934e358621d0785fd2920a1a304cebc9b7e47417e3fff780cfcddf7ca1177b45bbfac2f9978581761d9ddd55ec6142d92998eeadbafe8cd7651056fbb79afea643f0cd20ff4f6389ddd91ee2db4579438a6908609b4c1" + } + }, + { + "nodeID": "NodeID-P5QGH4EXddrcyNAzkqyZKHXgEpVX6HExL", + "rewardAddress": "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9", + "delegationFee": 62500, + "signer": { + "publicKey": "0xb0d35ccf70a6d84e2bca1dc166a4c3324d7edd86e879add2ba6511c8ef6bfd89a15534e67467ccd9c9211534b33295a1", + "proofOfPossession": "0xa4b480a9a07b4a97630dd9d6e2bf4834a3e670448b5575e3ba72a03e06ec509ec859840a1104b18f0cd54696e6f98adb0e9f5260161332fe32a50cb1a9806b1b5025037731ea77c641d607fd0584ce29d7954f5e8f523c13a2e5732511227f50" + } + }, + { + "nodeID": "NodeID-7eRvnfs2a2PvrPHUuCRRpPVAoVjbWxaFG", + "rewardAddress": "X-custom1v6vuwxjgr043sg0nuuhq70k6vgnule692fvnr9", + "delegationFee": 62500, + "signer": { + "publicKey": "0xa5fde6042c6e0ee482f46346df046000cd57dd878d36637a5a62adec07a5114cdeea094a85f4f72b46645f94e9076692", + "proofOfPossession": "0x91444230c5ceb8e51642139e1842b56f8563865376b6f4205beca4dc0bb0bc4b314bce1e19e3b542a3941ece51ae205e03a094824fee28f9f702ed310756f0637bf166371656e1c3eb9001df89f4cdccc73410242a6d875eb6363d12bce40316" + } + } + ], + "cChainGenesis": "{\"config\":{\"chainId\":43117,\"homesteadBlock\":0,\"daoForkBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0},\"nonce\":\"0x0\",\"timestamp\":\"0x0\",\"extraData\":\"0x00\",\"gasLimit\":\"0x5f5e100\",\"difficulty\":\"0x0\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"0100000000000000000000000000000000000000\":{\"code\":\"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033\",\"balance\":\"0x0\"},\"0x643F2454430E218750b5e6533d9C0e0Dd50B8d68\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0xf9BFA4C45a8d830a591B3374320fd8CCF3FD75D4\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0xD9d4f16a71E23eDf8e2F2a1Ebecd46B03177a22c\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0x2a17831425bc6D20084D1526b1001C451ED4C4A7\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0x7c5A8639F1e86F134f1E4239429f756A1441e322\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0xfDDEf5cb0D09E483dBAB587BA958657B79A42E58\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0xB4cA6C121D6287af7ac7cb62Ae33d2b054b9FC44\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"},\"0xC71A61a815e49d16C425482A342a367CD42E38a6\":{\"balance\":\"0x1431E0FAE6D7217CAA0000000\"}},\"number\":\"0x0\",\"gasUsed\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}", + "message": "Etna here we come" +} diff --git a/pkg/constants/etnaDevnet/upgrade.json b/pkg/constants/etnaDevnet/upgrade.json new file mode 100644 index 000000000..33cd0a5c9 --- /dev/null +++ b/pkg/constants/etnaDevnet/upgrade.json @@ -0,0 +1,16 @@ +{ + "apricotPhase1Time": "2020-12-05T05:00:00Z", + "apricotPhase2Time": "2020-12-05T05:00:00Z", + "apricotPhase3Time": "2020-12-05T05:00:00Z", + "apricotPhase4Time": "2020-12-05T05:00:00Z", + "apricotPhase4MinPChainHeight": 0, + "apricotPhase5Time": "2020-12-05T05:00:00Z", + "apricotPhasePre6Time": "2020-12-05T05:00:00Z", + "apricotPhase6Time": "2020-12-05T05:00:00Z", + "apricotPhasePost6Time": "2020-12-05T05:00:00Z", + "banffTime": "2020-12-05T05:00:00Z", + "cortinaTime": "2020-12-05T05:00:00Z", + "cortinaXChainStopVertexID": "11111111111111111111111111111111LpoYY", + "durangoTime": "2020-12-05T05:00:00Z", + "etnaTime": "2024-10-09T20:00:00Z" +} diff --git a/pkg/contract/allocations.go b/pkg/contract/allocations.go index 453950b80..8a097e930 100644 --- a/pkg/contract/allocations.go +++ b/pkg/contract/allocations.go @@ -71,7 +71,7 @@ func GetBlockchainAirdropKeyInfo( } } for address := range genesis.Alloc { - found, keyName, addressStr, privKey, err := searchForManagedKey(app, network, address, false) + found, keyName, addressStr, privKey, err := SearchForManagedKey(app, network, address, false) if err != nil { return "", "", "", err } @@ -82,7 +82,7 @@ func GetBlockchainAirdropKeyInfo( return "", "", "", nil } -func searchForManagedKey( +func SearchForManagedKey( app *application.Avalanche, network models.Network, address common.Address, @@ -204,7 +204,7 @@ func getGenesisNativeMinterAdmin( return false, false, "", "", "", nil } for _, admin := range allowListCfg.AllowListConfig.AdminAddresses { - found, keyName, addressStr, privKey, err := searchForManagedKey(app, network, admin, true) + found, keyName, addressStr, privKey, err := SearchForManagedKey(app, network, admin, true) if err != nil { return false, false, "", "", "", err } @@ -238,7 +238,7 @@ func getGenesisNativeMinterManager( return false, false, "", "", "", nil } for _, admin := range allowListCfg.AllowListConfig.ManagerAddresses { - found, keyName, addressStr, privKey, err := searchForManagedKey(app, network, admin, true) + found, keyName, addressStr, privKey, err := SearchForManagedKey(app, network, admin, true) if err != nil { return false, false, "", "", "", err } @@ -288,3 +288,19 @@ func GetEVMSubnetGenesisNativeMinterManager( } return getGenesisNativeMinterManager(app, network, genesisData) } + +func ContractAddressIsInGenesisData( + genesisData []byte, + contractAddress common.Address, +) (bool, error) { + genesis, err := utils.ByteSliceToSubnetEvmGenesis(genesisData) + if err != nil { + return false, err + } + for address, allocation := range genesis.Alloc { + if address == contractAddress { + return len(allocation.Code) > 0, nil + } + } + return false, nil +} diff --git a/pkg/contract/allocations_test.go b/pkg/contract/allocations_test.go new file mode 100644 index 000000000..07577ce94 --- /dev/null +++ b/pkg/contract/allocations_test.go @@ -0,0 +1,194 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package contract + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestContractAddressIsInGenesisData(t *testing.T) { + require := require.New(t) + + type test struct { + desc string + genesisData []byte + contractAddress common.Address + expected bool + shouldErr bool + } + + tests := []test{ + { + desc: "nil data", + genesisData: nil, + contractAddress: common.Address{}, + expected: false, + shouldErr: true, + }, + { + desc: "not json", + genesisData: []byte("not json"), + contractAddress: common.Address{}, + expected: false, + shouldErr: true, + }, + { + desc: "not evm", + genesisData: []byte("{}"), + contractAddress: common.Address{}, + expected: false, + shouldErr: true, + }, + { + desc: "no allocs", + genesisData: []byte(` +{ + "config": { + "byzantiumBlock": 0, "chainId": 1, "constantinopleBlock": 0, "eip150Block": 0, + "eip155Block": 0, "eip158Block": 0, + "feeConfig": { + "gasLimit": 12000000, "targetBlockRate": 2, "minBaseFee": 25000000000, + "targetGas": 60000000, "baseFeeChangeDenominator": 36, "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, "blockGasCostStep": 200000 + }, + "homesteadBlock": 0, "istanbulBlock": 0, "muirGlacierBlock": 0, "petersburgBlock": 0, + "warpConfig": { + "blockTimestamp": 1727309619, "quorumNumerator": 67 + } + }, + "nonce": "0x0", "timestamp": "0x66f4a733", "extraData": "0x", "gasLimit": "0xb71b00", "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "airdropHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "airdropAmount": null, "number": "0x0", "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": null, "excessBlobGas": null, "blobGasUsed": null, + "alloc": {} +} + `), + contractAddress: common.Address{}, + expected: false, + shouldErr: false, + }, + { + desc: "good path", + genesisData: []byte(` +{ + "config": { + "byzantiumBlock": 0, "chainId": 1, "constantinopleBlock": 0, "eip150Block": 0, + "eip155Block": 0, "eip158Block": 0, + "feeConfig": { + "gasLimit": 12000000, "targetBlockRate": 2, "minBaseFee": 25000000000, + "targetGas": 60000000, "baseFeeChangeDenominator": 36, "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, "blockGasCostStep": 200000 + }, + "homesteadBlock": 0, "istanbulBlock": 0, "muirGlacierBlock": 0, "petersburgBlock": 0, + "warpConfig": { + "blockTimestamp": 1727309619, "quorumNumerator": 67 + } + }, + "nonce": "0x0", "timestamp": "0x66f4a733", "extraData": "0x", "gasLimit": "0xb71b00", "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "airdropHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "airdropAmount": null, "number": "0x0", "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": null, "excessBlobGas": null, "blobGasUsed": null, + "alloc": { + "253b2784c75e510dd0ff1da844684a1ac0aa5fcf": { + "code": "0xfe", + "balance": "0x2086ac351052600000" + } + } +} + `), + contractAddress: common.HexToAddress("0x253b2784c75e510dd0ff1da844684a1ac0aa5fcf"), + expected: true, + shouldErr: false, + }, + { + desc: "no code", + genesisData: []byte(` +{ + "config": { + "byzantiumBlock": 0, "chainId": 1, "constantinopleBlock": 0, "eip150Block": 0, + "eip155Block": 0, "eip158Block": 0, + "feeConfig": { + "gasLimit": 12000000, "targetBlockRate": 2, "minBaseFee": 25000000000, + "targetGas": 60000000, "baseFeeChangeDenominator": 36, "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, "blockGasCostStep": 200000 + }, + "homesteadBlock": 0, "istanbulBlock": 0, "muirGlacierBlock": 0, "petersburgBlock": 0, + "warpConfig": { + "blockTimestamp": 1727309619, "quorumNumerator": 67 + } + }, + "nonce": "0x0", "timestamp": "0x66f4a733", "extraData": "0x", "gasLimit": "0xb71b00", "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "airdropHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "airdropAmount": null, "number": "0x0", "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": null, "excessBlobGas": null, "blobGasUsed": null, + "alloc": { + "253b2784c75e510dd0ff1da844684a1ac0aa5fcf": { + "balance": "0x2086ac351052600000" + } + } +} + `), + contractAddress: common.HexToAddress("0x253b2784c75e510dd0ff1da844684a1ac0aa5fcf"), + expected: false, + shouldErr: false, + }, + { + desc: "diff addr", + genesisData: []byte(` +{ + "config": { + "byzantiumBlock": 0, "chainId": 1, "constantinopleBlock": 0, "eip150Block": 0, + "eip155Block": 0, "eip158Block": 0, + "feeConfig": { + "gasLimit": 12000000, "targetBlockRate": 2, "minBaseFee": 25000000000, + "targetGas": 60000000, "baseFeeChangeDenominator": 36, "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, "blockGasCostStep": 200000 + }, + "homesteadBlock": 0, "istanbulBlock": 0, "muirGlacierBlock": 0, "petersburgBlock": 0, + "warpConfig": { + "blockTimestamp": 1727309619, "quorumNumerator": 67 + } + }, + "nonce": "0x0", "timestamp": "0x66f4a733", "extraData": "0x", "gasLimit": "0xb71b00", "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "airdropHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "airdropAmount": null, "number": "0x0", "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": null, "excessBlobGas": null, "blobGasUsed": null, + "alloc": { + "253b2784c75e510dd0ff1da844684a1ac0aa5fcf": { + "code": "0xfe", + "balance": "0x2086ac351052600000" + } + } +} + `), + contractAddress: common.HexToAddress("0x253b2724c75e510dd0ff1da844684a1ac0aa5fcc"), + expected: false, + shouldErr: false, + }, + } + + for _, t := range tests { + b, err := ContractAddressIsInGenesisData(t.genesisData, t.contractAddress) + if t.shouldErr { + require.Error(err, t.desc) + } else { + require.NoError(err, t.desc) + } + require.Equal(t.expected, b, t.desc) + } +} diff --git a/pkg/contract/chain.go b/pkg/contract/chain.go index 7459cc539..2acb0d539 100644 --- a/pkg/contract/chain.go +++ b/pkg/contract/chain.go @@ -181,8 +181,7 @@ func GetBlockchainEndpoints( rpcEndpoint string wsEndpoint string ) - switch { - case chainSpec.BlockchainName != "": + if chainSpec.BlockchainName != "" { sc, err := app.LoadSidecar(chainSpec.BlockchainName) if err != nil { return "", "", fmt.Errorf("failed to load sidecar: %w", err) @@ -205,16 +204,20 @@ func GetBlockchainEndpoints( if len(sc.Networks[networkName].WSEndpoints) > 0 { wsEndpoint = sc.Networks[networkName].WSEndpoints[0] } - case chainSpec.CChain: - rpcEndpoint = network.CChainEndpoint() - wsEndpoint = network.CChainWSEndpoint() - case network.Kind == models.Local: - blockchainID, err := GetBlockchainID(app, network, chainSpec) - if err != nil { - return "", "", err + } + if rpcEndpoint == "" { + switch { + case chainSpec.CChain: + rpcEndpoint = network.CChainEndpoint() + wsEndpoint = network.CChainWSEndpoint() + case network.Kind == models.Local: + blockchainID, err := GetBlockchainID(app, network, chainSpec) + if err != nil { + return "", "", err + } + rpcEndpoint = network.BlockchainEndpoint(blockchainID.String()) + wsEndpoint = network.BlockchainWSEndpoint(blockchainID.String()) } - rpcEndpoint = network.BlockchainEndpoint(blockchainID.String()) - wsEndpoint = network.BlockchainWSEndpoint(blockchainID.String()) } blockchainDesc, err := GetBlockchainDesc(chainSpec) if err != nil { diff --git a/pkg/contract/contract.go b/pkg/contract/contract.go index eeb051b4e..cfc879593 100644 --- a/pkg/contract/contract.go +++ b/pkg/contract/contract.go @@ -4,7 +4,9 @@ package contract import ( _ "embed" + "encoding/hex" "encoding/json" + "errors" "fmt" "math/big" "reflect" @@ -12,9 +14,13 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/evm" "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/subnet-evm/accounts/abi/bind" "github.com/ava-labs/subnet-evm/core/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" ) var ErrFailedReceiptStatus = fmt.Errorf("failed receipt status") @@ -283,11 +289,16 @@ func ParseSpec( return name, string(abiBytes), nil } +// get method name and types from [methodsSpec], then call it +// at the smart contract [contractAddress] with the given [params]. +// also send [payment] tokens to it func TxToMethod( rpcURL string, privateKey string, contractAddress common.Address, payment *big.Int, + description string, + errorSignatureToError map[string]error, methodSpec string, params ...interface{}, ) (*types.Transaction, *types.Receipt, error) { @@ -315,17 +326,209 @@ func TxToMethod( txOpts.Value = payment tx, err := contract.Transact(txOpts, methodName, params...) if err != nil { - return nil, nil, err + ux.Logger.PrintToUser("error on \"%s\": %s", description, err) + trace, traceCallErr := DebugTraceCall( + rpcURL, + privateKey, + contractAddress, + payment, + methodSpec, + params..., + ) + if traceCallErr != nil { + ux.Logger.PrintToUser("Could not get debug trace for %s error on %s: %s", description, rpcURL, traceCallErr) + ux.Logger.PrintToUser("Verify --debug flag value when calling 'blockchain create'") + return tx, nil, err + } + errorFromSignature, err := evm.GetErrorFromTrace(trace, errorSignatureToError) + if err != nil && !errors.Is(err, evm.ErrUnknownErrorSelector) { + ux.Logger.RedXToUser("failure traying to match error selector on trace: %s", err) + } + if errorFromSignature != nil { + return tx, nil, errorFromSignature + } else { + ux.Logger.PrintToUser("error trace for %s error:", description) + ux.Logger.PrintToUser("%#v", trace) + } + return tx, nil, err } receipt, success, err := evm.WaitForTransaction(client, tx) if err != nil { return tx, nil, err } else if !success { - return tx, receipt, ErrFailedReceiptStatus + return handleFailedReceiptStatus( + rpcURL, + description, + errorSignatureToError, + tx, + receipt, + ) + } + return tx, receipt, nil +} + +// get method name and types from [methodsSpec], then call it +// at the smart contract [contractAddress] with the given [params]. +// send [warpMessage] on the same call, whose signature is +// going to be verified previously to pass it to the method +// also send [payment] tokens to it +func TxToMethodWithWarpMessage( + rpcURL string, + privateKey string, + contractAddress common.Address, + warpMessage *avalancheWarp.Message, + payment *big.Int, + description string, + errorSignatureToError map[string]error, + methodSpec string, + params ...interface{}, +) (*types.Transaction, *types.Receipt, error) { + methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, false, false, params...) + if err != nil { + return nil, nil, err + } + metadata := &bind.MetaData{ + ABI: methodABI, + } + abi, err := metadata.GetAbi() + if err != nil { + return nil, nil, err + } + callData, err := abi.Pack(methodName, params...) + if err != nil { + return nil, nil, err + } + client, err := evm.GetClient(rpcURL) + if err != nil { + return nil, nil, err + } + defer client.Close() + tx, err := evm.GetSignedTxToMethodWithWarpMessage( + client, + privateKey, + warpMessage, + contractAddress, + callData, + payment, + ) + if err != nil { + return nil, nil, err + } + if err := evm.SendTransaction(client, tx); err != nil { + return tx, nil, err + } + receipt, success, err := evm.WaitForTransaction(client, tx) + if err != nil { + return tx, receipt, err + } else if !success { + return handleFailedReceiptStatus( + rpcURL, + description, + errorSignatureToError, + tx, + receipt, + ) } return tx, receipt, nil } +func handleFailedReceiptStatus( + rpcURL string, + description string, + errorSignatureToError map[string]error, + tx *types.Transaction, + receipt *types.Receipt, +) (*types.Transaction, *types.Receipt, error) { + trace, err := DebugTraceTransaction( + rpcURL, + tx.Hash().String(), + ) + if err != nil { + ux.Logger.PrintToUser( + "Could not get debug trace for %s error on %s, tx hash %s: %s", + description, + rpcURL, + tx.Hash(), + err, + ) + ux.Logger.PrintToUser("Verify --debug flag value when calling 'blockchain create'") + return tx, receipt, err + } + errorFromSignature, err := evm.GetErrorFromTrace(trace, errorSignatureToError) + if err != nil && !errors.Is(err, evm.ErrUnknownErrorSelector) { + ux.Logger.RedXToUser("failure traying to match error selector on trace: %s", err) + } + if errorFromSignature != nil { + return tx, receipt, errorFromSignature + } else { + ux.Logger.PrintToUser("error trace for %s error:", description) + ux.Logger.PrintToUser("%#v", trace) + } + return tx, receipt, ErrFailedReceiptStatus +} + +func DebugTraceTransaction( + rpcURL string, + txHash string, +) (map[string]interface{}, error) { + client, err := evm.GetRPCClient(rpcURL) + if err != nil { + return nil, err + } + return evm.DebugTraceTransaction( + client, + txHash, + ) +} + +func DebugTraceCall( + rpcURL string, + privateKey string, + contractAddress common.Address, + payment *big.Int, + methodSpec string, + params ...interface{}, +) (map[string]interface{}, error) { + methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, false, false, params...) + if err != nil { + return nil, err + } + metadata := &bind.MetaData{ + ABI: methodABI, + } + abi, err := metadata.GetAbi() + if err != nil { + return nil, err + } + callData, err := abi.Pack(methodName, params...) + if err != nil { + return nil, err + } + client, err := evm.GetRPCClient(rpcURL) + if err != nil { + return nil, err + } + defer client.Close() + pk, err := crypto.HexToECDSA(privateKey) + if err != nil { + return nil, err + } + from := crypto.PubkeyToAddress(pk.PublicKey) + data := map[string]string{ + "from": from.Hex(), + "to": contractAddress.Hex(), + "input": "0x" + hex.EncodeToString(callData), + } + if payment != nil { + hexBytes, _ := hexutil.Big(*payment).MarshalText() + data["value"] = string(hexBytes) + } + return evm.DebugTraceCall( + client, + data, + ) +} + func CallToMethod( rpcURL string, contractAddress common.Address, diff --git a/pkg/docker/compose.go b/pkg/docker/compose.go index 858db56fc..b246044dd 100644 --- a/pkg/docker/compose.go +++ b/pkg/docker/compose.go @@ -19,7 +19,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/ux" ) -type dockerComposeInputs struct { +type DockerComposeInputs struct { WithMonitoring bool WithAvalanchego bool AvalanchegoVersion string @@ -32,7 +32,7 @@ type dockerComposeInputs struct { //go:embed templates/*.docker-compose.yml var composeTemplate embed.FS -func renderComposeFile(composePath string, composeDesc string, templateVars dockerComposeInputs) ([]byte, error) { +func renderComposeFile(composePath string, composeDesc string, templateVars DockerComposeInputs) ([]byte, error) { compose, err := composeTemplate.ReadFile(composePath) if err != nil { return nil, err @@ -207,7 +207,7 @@ func ComposeOverSSH( host *models.Host, timeout time.Duration, composePath string, - composeVars dockerComposeInputs, + composeVars DockerComposeInputs, ) error { remoteComposeFile := utils.GetRemoteComposeFile() startTime := time.Now() diff --git a/pkg/docker/config.go b/pkg/docker/config.go index 0a9d501f0..8a8a303c9 100644 --- a/pkg/docker/config.go +++ b/pkg/docker/config.go @@ -5,6 +5,8 @@ package docker import ( "os" + "path/filepath" + "strings" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" @@ -12,11 +14,33 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" ) -func prepareAvalanchegoConfig(host *models.Host, network models.Network, publicAccess bool) (string, string, error) { +type AvalancheGoConfigOptions struct { + BootstrapIPs []string + BootstrapIDs []string + PartialSync bool + GenesisPath string + UpgradePath string + AllowPublicAccess bool +} + +func prepareAvalanchegoConfig( + host *models.Host, + network models.Network, + avalancheGoConfig AvalancheGoConfigOptions, +) (string, string, error) { avagoConf := remoteconfig.PrepareAvalancheConfig(host.IP, network.NetworkIDFlagValue(), nil) - if publicAccess || utils.IsE2E() { + if avalancheGoConfig.AllowPublicAccess || utils.IsE2E() { avagoConf.HTTPHost = "0.0.0.0" } + avagoConf.PartialSync = avalancheGoConfig.PartialSync + avagoConf.BootstrapIPs = strings.Join(avalancheGoConfig.BootstrapIPs, ",") + avagoConf.BootstrapIDs = strings.Join(avalancheGoConfig.BootstrapIDs, ",") + if avalancheGoConfig.GenesisPath != "" { + avagoConf.GenesisPath = filepath.Join(constants.DockerNodeConfigPath, constants.GenesisFileName) + } + if avalancheGoConfig.UpgradePath != "" { + avagoConf.UpgradePath = filepath.Join(constants.DockerNodeConfigPath, constants.UpgradeFileName) + } nodeConf, err := remoteconfig.RenderAvalancheNodeConfig(avagoConf) if err != nil { return "", "", err diff --git a/pkg/docker/ssh.go b/pkg/docker/ssh.go index 73a96e06f..aab8a089c 100644 --- a/pkg/docker/ssh.go +++ b/pkg/docker/ssh.go @@ -25,7 +25,18 @@ func ValidateComposeFile(host *models.Host, composeFile string, timeout time.Dur } // ComposeSSHSetupNode sets up an AvalancheGo node and dependencies on a remote host over SSH. -func ComposeSSHSetupNode(host *models.Host, network models.Network, avalancheGoVersion string, withMonitoring bool, publicAccessToHTTPPort bool) error { +func ComposeSSHSetupNode( + host *models.Host, + network models.Network, + avalancheGoVersion string, + avalanchegoBootstrapIDs []string, + avalanchegoBootstrapIPs []string, + partialSync bool, + avalanchegoGenesisFilePath string, + avalanchegoUpgradeFilePath string, + withMonitoring bool, + publicAccessToHTTPPort bool, +) error { startTime := time.Now() folderStructure := remoteconfig.RemoteFoldersToCreateAvalanchego() for _, dir := range folderStructure { @@ -41,7 +52,18 @@ func ComposeSSHSetupNode(host *models.Host, network models.Network, avalancheGoV return err } ux.Logger.Info("AvalancheGo Docker image %s ready on %s[%s] after %s", avagoDockerImage, host.NodeID, host.IP, time.Since(startTime)) - nodeConfFile, cChainConfFile, err := prepareAvalanchegoConfig(host, network, publicAccessToHTTPPort) + nodeConfFile, cChainConfFile, err := prepareAvalanchegoConfig( + host, + network, + AvalancheGoConfigOptions{ + BootstrapIDs: avalanchegoBootstrapIDs, + BootstrapIPs: avalanchegoBootstrapIPs, + PartialSync: partialSync, + GenesisPath: avalanchegoGenesisFilePath, + UpgradePath: avalanchegoUpgradeFilePath, + AllowPublicAccess: publicAccessToHTTPPort, + }, + ) if err != nil { return err } @@ -60,12 +82,22 @@ func ComposeSSHSetupNode(host *models.Host, network models.Network, avalancheGoV if err := host.Upload(cChainConfFile, remoteconfig.GetRemoteAvalancheCChainConfig(), constants.SSHFileOpsTimeout); err != nil { return err } + if avalanchegoGenesisFilePath != "" { + if err := host.Upload(avalanchegoGenesisFilePath, remoteconfig.GetRemoteAvalancheGenesis(), constants.SSHFileOpsTimeout); err != nil { + return err + } + } + if avalanchegoUpgradeFilePath != "" { + if err := host.Upload(avalanchegoUpgradeFilePath, remoteconfig.GetRemoteAvalancheUpgrade(), constants.SSHFileOpsTimeout); err != nil { + return err + } + } ux.Logger.Info("AvalancheGo configs uploaded to %s[%s] after %s", host.NodeID, host.IP, time.Since(startTime)) return ComposeOverSSH("Compose Node", host, constants.SSHScriptTimeout, "templates/avalanchego.docker-compose.yml", - dockerComposeInputs{ + DockerComposeInputs{ AvalanchegoVersion: avalancheGoVersion, WithMonitoring: withMonitoring, WithAvalanchego: true, @@ -80,7 +112,7 @@ func ComposeSSHSetupLoadTest(host *models.Host) error { host, constants.SSHScriptTimeout, "templates/avalanchego.docker-compose.yml", - dockerComposeInputs{ + DockerComposeInputs{ WithMonitoring: true, WithAvalanchego: false, }) @@ -133,7 +165,7 @@ func ComposeSSHSetupMonitoring(host *models.Host) error { host, constants.SSHScriptTimeout, "templates/monitoring.docker-compose.yml", - dockerComposeInputs{}) + DockerComposeInputs{}) } func ComposeSSHSetupAWMRelayer(host *models.Host, relayerVersion string) error { @@ -141,7 +173,7 @@ func ComposeSSHSetupAWMRelayer(host *models.Host, relayerVersion string) error { host, constants.SSHScriptTimeout, "templates/awmrelayer.docker-compose.yml", - dockerComposeInputs{ + DockerComposeInputs{ AWMRelayerVersion: relayerVersion, }) } diff --git a/pkg/evm/evm.go b/pkg/evm/evm.go index 2982c0279..38fa7affe 100644 --- a/pkg/evm/evm.go +++ b/pkg/evm/evm.go @@ -3,7 +3,9 @@ package evm import ( + "context" "crypto/ecdsa" + "encoding/hex" "fmt" "math/big" "net/url" @@ -12,11 +14,16 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/subnet-evm/accounts/abi/bind" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/ethclient" + "github.com/ava-labs/subnet-evm/interfaces" + "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/contracts/warp" + "github.com/ava-labs/subnet-evm/predicate" "github.com/ava-labs/subnet-evm/rpc" - subnetEvmUtils "github.com/ava-labs/subnet-evm/tests/utils" + subnetEvmUtils "github.com/ava-labs/subnet-evm/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -29,6 +36,8 @@ const ( sleepBetweenRepeats = 1 * time.Second ) +var ErrUnknownErrorSelector = fmt.Errorf("unknown error selector") + func ContractAlreadyDeployed( client ethclient.Client, contractAddress string, @@ -178,6 +187,27 @@ func EstimateBaseFee( return baseFee, err } +func EstimateGasLimit( + client ethclient.Client, + msg interfaces.CallMsg, +) (uint64, error) { + var ( + gasLimit uint64 + err error + ) + for i := 0; i < repeatsOnFailure; i++ { + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + gasLimit, err = client.EstimateGas(ctx, msg) + if err == nil { + break + } + err = fmt.Errorf("failure estimating gas limit on %#v: %w", client, err) + time.Sleep(sleepBetweenRepeats) + } + return gasLimit, err +} + func FundAddress( client ethclient.Client, sourceAddressPrivateKeyStr string, @@ -223,6 +253,66 @@ func FundAddress( return nil } +func GetSignedTxToMethodWithWarpMessage( + client ethclient.Client, + privateKeyStr string, + warpMessage *avalancheWarp.Message, + contract common.Address, + callData []byte, + value *big.Int, +) (*types.Transaction, error) { + const defaultGasLimit = 2_000_000 + privateKey, err := crypto.HexToECDSA(privateKeyStr) + if err != nil { + return nil, err + } + address := crypto.PubkeyToAddress(privateKey.PublicKey) + gasFeeCap, gasTipCap, nonce, err := CalculateTxParams(client, address.Hex()) + if err != nil { + return nil, err + } + chainID, err := GetChainID(client) + if err != nil { + return nil, err + } + accessList := types.AccessList{ + types.AccessTuple{ + Address: warp.ContractAddress, + StorageKeys: subnetEvmUtils.BytesToHashSlice(predicate.PackPredicate(warpMessage.Bytes())), + }, + } + msg := interfaces.CallMsg{ + From: address, + To: &contract, + GasPrice: nil, + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Value: value, + Data: callData, + AccessList: accessList, + } + gasLimit, err := EstimateGasLimit(client, msg) + if err != nil { + // assuming this is related to the tx itself. + // just using default gas limit, and let the user debug the + // tx if needed so + gasLimit = defaultGasLimit + } + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &contract, + Gas: gasLimit, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Value: value, + Data: callData, + AccessList: accessList, + }) + txSigner := types.LatestSignerForChainID(chainID) + return types.SignTx(tx, txSigner, privateKey) +} + func IssueTx( client ethclient.Client, txStr string, @@ -345,6 +435,30 @@ func GetClient(rpcURL string) (ethclient.Client, error) { return client, err } +func WaitForChainID(client ethclient.Client) { + startTime := time.Now() + spinSession := ux.NewUserSpinner() + spinner := spinSession.SpinToUser("Checking if node is healthy...") + for { + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + _, err := client.ChainID(ctx) + if err == nil { + ux.SpinComplete(spinner) + spinSession.Stop() + ux.Logger.GreenCheckmarkToUser("Node is healthy after %d seconds", uint32(time.Since(startTime).Seconds())) + break + } else { + if time.Since(startTime) > 60*time.Second { + ux.SpinFailWithError(spinner, "", fmt.Errorf("failure getting chain id from client %#v: %w", client, err)) + spinSession.Stop() + break + } + time.Sleep(5 * time.Second) + } + } +} + func GetChainID(client ethclient.Client) (*big.Int, error) { var ( chainID *big.Int @@ -424,10 +538,23 @@ func GetRPCClient(rpcURL string) (*rpc.Client, error) { client *rpc.Client err error ) + hasScheme, err := HasScheme(rpcURL) + if err != nil { + return nil, err + } for i := 0; i < repeatsOnFailure; i++ { ctx, cancel := utils.GetAPILargeContext() defer cancel() - client, err = rpc.DialContext(ctx, rpcURL) + if !hasScheme { + _, scheme, findErr := FindOutScheme(rpcURL) + if findErr == nil { + client, err = rpc.DialContext(ctx, scheme+rpcURL) + } else { + err = findErr + } + } else { + client, err = rpc.DialContext(ctx, rpcURL) + } if err == nil { break } @@ -460,7 +587,39 @@ func DebugTraceTransaction( break } err = fmt.Errorf("failure tracing tx %s for client %#v: %w", txID, client, err) - ux.Logger.RedXToUser("%s", err) + time.Sleep(sleepBetweenRepeats) + } + return trace, err +} + +func DebugTraceCall( + client *rpc.Client, + toTrace map[string]string, +) (map[string]interface{}, error) { + var ( + err error + trace map[string]interface{} + ) + for i := 0; i < repeatsOnFailure; i++ { + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + err = client.CallContext( + ctx, + &trace, + "debug_traceCall", + toTrace, + "latest", + map[string]interface{}{ + "tracer": "callTracer", + "tracerConfig": map[string]interface{}{ + "onlyTopCall": false, + }, + }, + ) + if err == nil { + break + } + err = fmt.Errorf("failure tracing call for client %#v: %w", client, err) time.Sleep(sleepBetweenRepeats) } return trace, err @@ -498,11 +657,12 @@ func IssueTxsToActivateProposerVMFork( chainID *big.Int, privKey *ecdsa.PrivateKey, ) error { + var errorList []error var err error for i := 0; i < repeatsOnFailure; i++ { ctx, cancel := utils.GetAPILargeContext() defer cancel() - err = subnetEvmUtils.IssueTxsToActivateProposerVMFork(ctx, chainID, privKey, client) + err = issueTxsToActivateProposerVMFork(client, ctx, chainID, privKey) if err == nil { break } @@ -511,8 +671,164 @@ func IssueTxsToActivateProposerVMFork( client, err, ) - ux.Logger.RedXToUser("%s", err) + errorList = append(errorList, err) time.Sleep(sleepBetweenRepeats) } + // this means that on the last try there is error + // print out all previous errors + if err != nil { + for _, indivError := range errorList { + ux.Logger.RedXToUser("%s", indivError) + } + } return err } + +// issueTxsToActivateProposerVMFork issues transactions at the current +// timestamp, which should be after the ProposerVM activation time (aka +// ApricotPhase4). This should generate a PostForkBlock because its parent block +// (genesis) has a timestamp (0) that is greater than or equal to the fork +// activation time of 0. Therefore, subsequent blocks should be built with +// BuildBlockWithContext. +func issueTxsToActivateProposerVMFork( + client ethclient.Client, + ctx context.Context, + chainID *big.Int, + fundedKey *ecdsa.PrivateKey, +) error { + const numTriggerTxs = 2 // Number of txs needed to activate the proposer VM fork + addr := crypto.PubkeyToAddress(fundedKey.PublicKey) + gasPrice := big.NewInt(params.MinGasPrice) + txSigner := types.LatestSignerForChainID(chainID) + for i := 0; i < numTriggerTxs; i++ { + prevBlockNumber, err := client.BlockNumber(ctx) + if err != nil { + return err + } + nonce, err := client.NonceAt(ctx, addr, nil) + if err != nil { + return err + } + tx := types.NewTransaction( + nonce, addr, common.Big1, params.TxGas, gasPrice, nil) + triggerTx, err := types.SignTx(tx, txSigner, fundedKey) + if err != nil { + return err + } + if err := client.SendTransaction(ctx, triggerTx); err != nil { + return err + } + if err := WaitForNewBlock(client, ctx, prevBlockNumber, 0, 0); err != nil { + return err + } + } + return nil +} + +func WaitForNewBlock( + client ethclient.Client, + ctx context.Context, + prevBlockNumber uint64, + totalDuration time.Duration, + stepDuration time.Duration, +) error { + if stepDuration == 0 { + stepDuration = 1 * time.Second + } + if totalDuration == 0 { + totalDuration = 10 * time.Second + } + steps := totalDuration / stepDuration + for seconds := 0; seconds < int(steps); seconds++ { + blockNumber, err := client.BlockNumber(ctx) + if err != nil { + return err + } + if blockNumber > prevBlockNumber { + return nil + } + time.Sleep(stepDuration) + } + return fmt.Errorf("new block not produced in %f seconds", totalDuration.Seconds()) +} + +func ExtractWarpMessageFromReceipt( + client ethclient.Client, + ctx context.Context, + receipt *types.Receipt, +) (*avalancheWarp.UnsignedMessage, error) { + logs, err := client.FilterLogs(ctx, interfaces.FilterQuery{ + BlockHash: &receipt.BlockHash, + Addresses: []common.Address{warp.Module.Address}, + }) + if err != nil { + return nil, err + } + if len(logs) != 1 { + return nil, fmt.Errorf("expected block to contain 1 warp log, got %d", len(logs)) + } + txLog := logs[0] + return warp.UnpackSendWarpEventDataToMessage(txLog.Data) +} + +func GetFunctionSelector(functionSignature string) string { + return "0x" + hex.EncodeToString(crypto.Keccak256([]byte(functionSignature))[:4]) +} + +func GetErrorFromTrace( + trace map[string]interface{}, + functionSignatureToError map[string]error, +) (error, error) { + traceOutputI, ok := trace["output"] + if !ok { + return nil, fmt.Errorf("trace does not contain output field") + } + traceOutput, ok := traceOutputI.(string) + if !ok { + return nil, fmt.Errorf("expected type string for trace output, got %T", traceOutputI) + } + traceOutputBytes, err := hex.DecodeString(strings.TrimPrefix(traceOutput, "0x")) + if err != nil { + return nil, fmt.Errorf("failure decoding trace output: %w", err) + } + if len(traceOutputBytes) < 4 { + return nil, fmt.Errorf("less than 4 bytes in trace output") + } + traceErrorSelector := "0x" + hex.EncodeToString(traceOutputBytes[:4]) + for errorSignature, err := range functionSignatureToError { + errorSelector := GetFunctionSelector(errorSignature) + if traceErrorSelector == errorSelector { + return err, nil + } + } + return nil, fmt.Errorf("%w: %s", ErrUnknownErrorSelector, traceErrorSelector) +} + +func TransactionError(tx *types.Transaction, err error, msg string, args ...interface{}) error { + msgSuffix := ": %w" + if tx != nil { + msgSuffix += fmt.Sprintf(" (txHash=%s)", tx.Hash().String()) + } else { + msgSuffix += " (tx failed to be submitted)" + } + args = append(args, err) + return fmt.Errorf(msg+msgSuffix, args...) +} + +func WaitForRPC(ctx context.Context, rpcURL string) error { + client, err := GetClient(rpcURL) + if err != nil { + return err + } + for { + _, err := client.ChainID(ctx) + if err == nil { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1 * time.Second): + } + } +} diff --git a/pkg/ictt/deploy.go b/pkg/ictt/deploy.go index 657de44fd..dd98a573e 100644 --- a/pkg/ictt/deploy.go +++ b/pkg/ictt/deploy.go @@ -41,6 +41,8 @@ func RegisterRemote( privateKey, remoteAddress, nil, + "register remote with home", + nil, "registerWithHome((address, uint256))", feeInfo, ) diff --git a/pkg/ictt/operate.go b/pkg/ictt/operate.go index 02689dc85..00edabe65 100644 --- a/pkg/ictt/operate.go +++ b/pkg/ictt/operate.go @@ -231,6 +231,8 @@ func ERC20TokenHomeSend( privateKey, tokenAddress, nil, + "erc20 token approve", + nil, "approve(address, uint256)->(bool)", homeAddress, amount, @@ -252,6 +254,8 @@ func ERC20TokenHomeSend( privateKey, homeAddress, nil, + "erc20 token home send", + nil, "send((bytes32, address, address, address, uint256, uint256, uint256, address), uint256)", params, amount, @@ -297,6 +301,8 @@ func NativeTokenHomeSend( privateKey, homeAddress, amount, + "native token home send", + nil, "send((bytes32, address, address, address, uint256, uint256, uint256, address))", params, ) @@ -317,6 +323,8 @@ func ERC20TokenRemoteSend( privateKey, remoteAddress, nil, + "erc20 token remote approve", + nil, "approve(address, uint256)->(bool)", remoteAddress, amount, @@ -348,6 +356,8 @@ func ERC20TokenRemoteSend( privateKey, remoteAddress, nil, + "erc20 token remote send", + nil, "send((bytes32, address, address, address, uint256, uint256, uint256, address), uint256)", params, amount, @@ -389,6 +399,8 @@ func NativeTokenRemoteSend( privateKey, remoteAddress, amount, + "native token remote send", + nil, "send((bytes32, address, address, address, uint256, uint256, uint256, address))", params, ) @@ -408,6 +420,8 @@ func NativeTokenHomeAddCollateral( privateKey, homeAddress, amount, + "native token home add collateral", + nil, "addCollateral(bytes32, address)", remoteBlockchainID, remoteAddress, @@ -432,6 +446,8 @@ func ERC20TokenHomeAddCollateral( privateKey, tokenAddress, nil, + "erc20 token home approve", + nil, "approve(address, uint256)->(bool)", homeAddress, amount, @@ -443,6 +459,8 @@ func ERC20TokenHomeAddCollateral( privateKey, homeAddress, nil, + "erc20 token home add collateral", + nil, "addCollateral(bytes32, address, uint256)", remoteBlockchainID, remoteAddress, diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 323a7898d..a595535de 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -120,7 +120,6 @@ func GetKeychainFromCmdLineFlags( if len(ledgerAddresses) > 0 { useLedger = true } - // check mutually exclusive flags if !flags.EnsureMutuallyExclusive([]bool{useLedger, useEwoq, keyName != ""}) { return nil, ErrMutuallyExlusiveKeySource @@ -135,6 +134,15 @@ func GetKeychainFromCmdLineFlags( return nil, err } } + case network.Kind == models.EtnaDevnet: + // prompt the user if no key source was provided + if !useEwoq && !useLedger && keyName == "" { + var err error + useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, keychainGoal, app.GetKeyDir(), true) + if err != nil { + return nil, err + } + } case network.Kind == models.Devnet: // prompt the user if no key source was provided if !useEwoq && !useLedger && keyName == "" { diff --git a/pkg/localnet/localnet.go b/pkg/localnet/localnet.go index e86f3ddd1..1ebcf57a2 100644 --- a/pkg/localnet/localnet.go +++ b/pkg/localnet/localnet.go @@ -3,7 +3,9 @@ package localnet import ( + "context" "encoding/json" + "fmt" "os" "path/filepath" "strings" @@ -12,7 +14,9 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-network-runner/client" "github.com/ava-labs/avalanche-network-runner/rpcpb" + "github.com/ava-labs/avalanche-network-runner/server" ) func GetClusterInfo() (*rpcpb.ClusterInfo, error) { @@ -92,3 +96,14 @@ func Deployed(subnetName string) (bool, error) { } return true, nil } + +func CheckNetworkIsAlreadyBootstrapped(ctx context.Context, cli client.Client) (bool, error) { + _, err := cli.Status(ctx) + if err != nil { + if server.IsServerError(err, server.ErrNotBootstrapped) { + return false, nil + } + return false, fmt.Errorf("failed trying to get network status: %w", err) + } + return true, nil +} diff --git a/pkg/localnet/output.go b/pkg/localnet/output.go index c71b77d80..29d971931 100644 --- a/pkg/localnet/output.go +++ b/pkg/localnet/output.go @@ -61,9 +61,7 @@ func PrintSubnetEndpoints( {Number: 1, AutoMerge: true}, }) t.SetTitle(fmt.Sprintf("%s RPC URLs", chainInfo.ChainName)) - aliasedURL := fmt.Sprintf("%s/ext/bc/%s/rpc", (*nodeInfo).GetUri(), chainInfo.ChainName) blockchainIDURL := fmt.Sprintf("%s/ext/bc/%s/rpc", (*nodeInfo).GetUri(), chainInfo.ChainId) - t.AppendRow(table.Row{"Localhost", aliasedURL}) t.AppendRow(table.Row{"Localhost", blockchainIDURL}) if utils.InsideCodespace() { var err error @@ -71,11 +69,6 @@ func PrintSubnetEndpoints( if err != nil { return err } - aliasedURL, err = utils.GetCodespaceURL(aliasedURL) - if err != nil { - return err - } - t.AppendRow(table.Row{"Codespace", aliasedURL}) t.AppendRow(table.Row{"Codespace", blockchainIDURL}) } printFunc(t.Render()) diff --git a/pkg/models/bootstrap_validator.go b/pkg/models/bootstrap_validator.go new file mode 100644 index 000000000..8072bf39a --- /dev/null +++ b/pkg/models/bootstrap_validator.go @@ -0,0 +1,17 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package models + +type SubnetValidator struct { + NodeID string `json:"NodeID"` + + Weight uint64 `json:"Weight"` + + Balance uint64 `json:"Balance"` + + BLSPublicKey string `json:"BLSPublicKey"` + + BLSProofOfPossession string `json:"BLSProofOfPossession"` + + ChangeOwnerAddr string `json:"ChangeOwnerAddr"` +} diff --git a/pkg/models/clusters_config.go b/pkg/models/clusters_config.go index cec654905..02d5b11b2 100644 --- a/pkg/models/clusters_config.go +++ b/pkg/models/clusters_config.go @@ -28,6 +28,7 @@ type ClusterConfig struct { ExtraNetworkData ExtraNetworkData Subnets []string External bool + Local bool HTTPAccess constants.HTTPAccess } @@ -53,14 +54,17 @@ func (cc *ClusterConfig) GetValidatorHosts(hosts []*Host) []*Host { } func (cc *ClusterConfig) IsAPIHost(hostCloudID string) bool { - return slices.Contains(cc.APINodes, hostCloudID) + return cc.Local || slices.Contains(cc.APINodes, hostCloudID) } func (cc *ClusterConfig) IsAvalancheGoHost(hostCloudID string) bool { - return slices.Contains(cc.Nodes, hostCloudID) + return cc.Local || slices.Contains(cc.Nodes, hostCloudID) } func (cc *ClusterConfig) GetCloudIDs() []string { + if cc.Local { + return nil + } r := cc.Nodes if cc.MonitoringInstance != "" { r = append(r, cc.MonitoringInstance) diff --git a/pkg/models/network.go b/pkg/models/network.go index 36581e138..04db90794 100644 --- a/pkg/models/network.go +++ b/pkg/models/network.go @@ -3,10 +3,14 @@ package models import ( + "context" "fmt" "os" "strings" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanchego/genesis" avagoconstants "github.com/ava-labs/avalanchego/utils/constants" @@ -20,6 +24,7 @@ const ( Fuji Local Devnet + EtnaDevnet ) func (nk NetworkKind) String() string { @@ -32,6 +37,8 @@ func (nk NetworkKind) String() string { return "Local Network" case Devnet: return "Devnet" + case EtnaDevnet: + return "Etna Devnet" } return "invalid network" } @@ -54,6 +61,10 @@ func NewNetwork(kind NetworkKind, id uint32, endpoint string, clusterName string } } +func (n Network) IsUndefined() bool { + return n.Kind == Undefined +} + func NewLocalNetwork() Network { return NewNetwork(Local, constants.LocalNetworkID, constants.LocalAPIEndpoint, "") } @@ -68,6 +79,41 @@ func NewDevnetNetwork(endpoint string, id uint32) Network { return NewNetwork(Devnet, id, endpoint, "") } +// ConvertClusterToNetwork converts a cluster network into a non cluster network +func ConvertClusterToNetwork(clusterNetwork Network) Network { + if clusterNetwork.ClusterName == "" { + return clusterNetwork + } + switch { + case clusterNetwork.ID == constants.LocalNetworkID: + return NewLocalNetwork() + case clusterNetwork.ID == avagoconstants.FujiID: + return NewFujiNetwork() + case clusterNetwork.ID == avagoconstants.MainnetID: + return NewMainnetNetwork() + case clusterNetwork.ID == constants.EtnaDevnetNetworkID: + return NewEtnaDevnetNetwork() + default: + networkID := uint32(0) + if clusterNetwork.Endpoint != "" { + infoClient := info.NewClient(clusterNetwork.Endpoint) + ctx, cancel := utils.GetAPIContext() + defer cancel() + var err error + networkID, err = infoClient.GetNetworkID(ctx) + if err != nil { + return clusterNetwork + } + return NewDevnetNetwork(clusterNetwork.Endpoint, networkID) + } + return clusterNetwork + } +} + +func NewEtnaDevnetNetwork() Network { + return NewNetwork(EtnaDevnet, constants.EtnaDevnetNetworkID, constants.EtnaDevnetEndpoint, "") +} + func NewFujiNetwork() Network { return NewNetwork(Fuji, avagoconstants.FujiID, constants.FujiAPIEndpoint, "") } @@ -97,7 +143,7 @@ func (n Network) StandardPublicEndpoint() bool { } func (n Network) Name() string { - if n.ClusterName != "" && n.Kind == Devnet { + if n.ClusterName != "" && (n.Kind == Devnet || n.Kind == EtnaDevnet) { return "Cluster " + n.ClusterName } name := n.Kind.String() @@ -137,6 +183,8 @@ func (n Network) NetworkIDFlagValue() string { switch n.Kind { case Local: return fmt.Sprintf("network-%d", n.ID) + case EtnaDevnet: + return fmt.Sprintf("%d", n.ID) case Devnet: return fmt.Sprintf("network-%d", n.ID) case Fuji: @@ -151,6 +199,8 @@ func (n Network) GenesisParams() *genesis.Params { switch n.Kind { case Local: return &genesis.LocalParams + case EtnaDevnet: + return &genesis.LocalParams // use LocalParams for now case Devnet: return &genesis.LocalParams case Fuji: @@ -174,3 +224,29 @@ func (n *Network) HandlePublicNetworkSimulation() { func (n *Network) Equals(n2 Network) bool { return n.Kind == n2.Kind && n.Endpoint == n2.Endpoint } + +// Context for bootstrapping a partial synced Node +func (n *Network) BootstrappingContext() (context.Context, context.CancelFunc) { + timeout := constants.ANRRequestTimeout + if n.Kind == Fuji { + timeout = constants.FujiBootstrapTimeout + } + return context.WithTimeout(context.Background(), timeout) +} + +// GetNetworkFromCluster gets the network that a cluster is on +func GetNetworkFromCluster(clusterConfig ClusterConfig) Network { + network := clusterConfig.Network + switch { + case network.ID == constants.LocalNetworkID: + return NewLocalNetwork() + case network.ID == avagoconstants.FujiID: + return NewFujiNetwork() + case network.ID == avagoconstants.MainnetID: + return NewMainnetNetwork() + case network.ID == constants.EtnaDevnetNetworkID: + return NewEtnaDevnetNetwork() + default: + return network + } +} diff --git a/pkg/models/sidecar.go b/pkg/models/sidecar.go index b7f5956ec..c08676ffc 100644 --- a/pkg/models/sidecar.go +++ b/pkg/models/sidecar.go @@ -15,6 +15,8 @@ type NetworkData struct { TeleporterRegistryAddress string RPCEndpoints []string WSEndpoints []string + BootstrapValidators []SubnetValidator + ClusterName string } type Sidecar struct { @@ -41,6 +43,12 @@ type Sidecar struct { RunRelayer bool // SubnetEVM based VM's only SubnetEVMMainnetChainID uint + // TODO: remove if not needed for subnet acp 77 create flow once avalnache go releases etna + ValidatorManagement ValidatorManagementType + ValidatorManagerOwner string + ProxyContractOwner string + // Subnet defaults to Sovereign post ACP-77 + Sovereign bool } func (sc Sidecar) GetVMID() (string, error) { @@ -57,3 +65,16 @@ func (sc Sidecar) GetVMID() (string, error) { } return vmid, nil } + +func (sc Sidecar) NetworkDataIsEmpty(network string) bool { + _, networkExists := sc.Networks[network] + return !networkExists +} + +func (sc Sidecar) PoA() bool { + return sc.ValidatorManagement == ProofOfAuthority +} + +func (sc Sidecar) PoS() bool { + return sc.ValidatorManagement == ProofOfStake +} diff --git a/pkg/models/validator_management.go b/pkg/models/validator_management.go new file mode 100644 index 000000000..9af44f84d --- /dev/null +++ b/pkg/models/validator_management.go @@ -0,0 +1,22 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package models + +type ValidatorManagementType string + +const ( + ProofOfStake = "Proof Of Stake" + ProofOfAuthority = "Proof Of Authority" + UndefinedValidatorManagement = "Undefined Validator Management" +) + +func ValidatorManagementTypeFromString(s string) ValidatorManagementType { + switch s { + case ProofOfStake: + return ProofOfStake + case ProofOfAuthority: + return ProofOfAuthority + default: + return UndefinedValidatorManagement + } +} diff --git a/pkg/networkoptions/network_options.go b/pkg/networkoptions/network_options.go index 52cebbac1..f8c0910a4 100644 --- a/pkg/networkoptions/network_options.go +++ b/pkg/networkoptions/network_options.go @@ -27,6 +27,7 @@ const ( Mainnet Fuji Local + EtnaDevnet Devnet Cluster ) @@ -39,6 +40,8 @@ func (n NetworkOption) String() string { return "Fuji Testnet" case Local: return "Local Network" + case EtnaDevnet: + return "Etna Devnet" case Devnet: return "Devnet" case Cluster: @@ -47,29 +50,33 @@ func (n NetworkOption) String() string { return "invalid network" } -func networkOptionFromString(s string) NetworkOption { - switch s { - case "Mainnet": +func NetworkOptionFromString(s string) NetworkOption { + switch { + case s == "Mainnet": return Mainnet - case "Fuji Testnet": + case s == "Fuji Testnet": return Fuji - case "Local Network": + case s == "Local Network": return Local - case "Devnet": + case s == "Etna Devnet": + return EtnaDevnet + case s == "Devnet" || strings.Contains(s, "Devnet"): return Devnet - case "Cluster": + case s == "Cluster" || strings.Contains(s, "Cluster"): return Cluster + default: + return Undefined } - return Undefined } type NetworkFlags struct { - UseLocal bool - UseDevnet bool - UseFuji bool - UseMainnet bool - Endpoint string - ClusterName string + UseLocal bool + UseEtnaDevnet bool + UseDevnet bool + UseFuji bool + UseMainnet bool + Endpoint string + ClusterName string } func AddNetworkFlagsToCmd(cmd *cobra.Command, networkFlags *NetworkFlags, addEndpoint bool, supportedNetworkOptions []NetworkOption) { @@ -78,6 +85,8 @@ func AddNetworkFlagsToCmd(cmd *cobra.Command, networkFlags *NetworkFlags, addEnd switch networkOption { case Local: cmd.Flags().BoolVarP(&networkFlags.UseLocal, "local", "l", false, "operate on a local network") + case EtnaDevnet: + cmd.Flags().BoolVarP(&networkFlags.UseEtnaDevnet, "etna-devnet", "", false, "operate on an etna devnet network") case Devnet: cmd.Flags().BoolVar(&networkFlags.UseDevnet, "devnet", false, "operate on a devnet network") addEndpoint = true @@ -164,6 +173,21 @@ func GetSupportedNetworkOptionsForSubnet( return filteredSupportedNetworkOptions, clusterNames, devnetEndpoints, nil } +func GetNetworkFromSidecar(sc models.Sidecar, defaultOption []NetworkOption) []NetworkOption { + networkOptionsList := []NetworkOption{} + for scNetwork := range sc.Networks { + if NetworkOptionFromString(scNetwork) != Undefined { + networkOptionsList = append(networkOptionsList, NetworkOptionFromString(scNetwork)) + } + } + + // default network options to add validator options + if len(networkOptionsList) == 0 { + networkOptionsList = defaultOption + } + return networkOptionsList +} + func GetNetworkFromCmdLineFlags( app *application.Avalanche, promptStr string, @@ -197,11 +221,12 @@ func GetNetworkFromCmdLineFlags( } // supported flags networkFlagsMap := map[NetworkOption]string{ - Local: "--local", - Devnet: "--devnet", - Fuji: "--fuji/--testnet", - Mainnet: "--mainnet", - Cluster: "--cluster", + Local: "--local", + EtnaDevnet: "--etna-devnet", + Devnet: "--devnet", + Fuji: "--fuji/--testnet", + Mainnet: "--mainnet", + Cluster: "--cluster", } supportedNetworksFlags := strings.Join(utils.Map(supportedNetworkOptions, func(n NetworkOption) string { return networkFlagsMap[n] }), ", ") // received option @@ -209,6 +234,8 @@ func GetNetworkFromCmdLineFlags( switch { case networkFlags.UseLocal: networkOption = Local + case networkFlags.UseEtnaDevnet: + networkOption = EtnaDevnet case networkFlags.UseDevnet: networkOption = Devnet case networkFlags.UseFuji: @@ -230,7 +257,9 @@ func GetNetworkFromCmdLineFlags( } } // unsupported option - if networkOption != Undefined && !slices.Contains(supportedNetworkOptions, networkOption) { + // allow cluster because we can extract underlying network from cluster + // don't check for unsupported network on e2e run + if networkOption != Undefined && !slices.Contains(supportedNetworkOptions, networkOption) && networkOption != Cluster && os.Getenv(constants.SimulatePublicNetwork) == "" { errMsg := fmt.Errorf("network flag %s is not supported. use one of %s", networkFlagsMap[networkOption], supportedNetworksFlags) if subnetName != "" { clustersMsg := "" @@ -246,7 +275,7 @@ func GetNetworkFromCmdLineFlags( return models.UndefinedNetwork, errMsg } // mutual exclusion - if !flags.EnsureMutuallyExclusive([]bool{networkFlags.UseLocal, networkFlags.UseDevnet, networkFlags.UseFuji, networkFlags.UseMainnet, networkFlags.ClusterName != ""}) { + if !flags.EnsureMutuallyExclusive([]bool{networkFlags.UseLocal, networkFlags.UseEtnaDevnet, networkFlags.UseDevnet, networkFlags.UseFuji, networkFlags.UseMainnet, networkFlags.ClusterName != ""}) { return models.UndefinedNetwork, fmt.Errorf("network flags %s are mutually exclusive", supportedNetworksFlags) } @@ -279,7 +308,7 @@ func GetNetworkFromCmdLineFlags( if err != nil { return models.UndefinedNetwork, err } - networkOption = networkOptionFromString(networkOptionStr) + networkOption = NetworkOptionFromString(networkOptionStr) if networkOption == Devnet && !onlyEndpointBasedDevnets && len(clusterNames) != 0 { endpointOptions := []string{ "Get Devnet RPC endpoint from an existing node cluster (created from avalanche node create or avalanche devnet wiz)", @@ -334,6 +363,8 @@ func GetNetworkFromCmdLineFlags( switch networkOption { case Local: network = models.NewLocalNetwork() + case EtnaDevnet: + network = models.NewEtnaDevnetNetwork() case Devnet: networkID := uint32(0) if networkFlags.Endpoint != "" { @@ -345,7 +376,11 @@ func GetNetworkFromCmdLineFlags( return models.UndefinedNetwork, err } } - network = models.NewDevnetNetwork(networkFlags.Endpoint, networkID) + if networkFlags.Endpoint == constants.EtnaDevnetEndpoint { + network = models.NewEtnaDevnetNetwork() + } else { + network = models.NewDevnetNetwork(networkFlags.Endpoint, networkID) + } case Fuji: network = models.NewFujiNetwork() case Mainnet: diff --git a/pkg/node/helper.go b/pkg/node/helper.go new file mode 100644 index 000000000..26a525a96 --- /dev/null +++ b/pkg/node/helper.go @@ -0,0 +1,434 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +import ( + "encoding/json" + "errors" + "fmt" + "sync" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/binutils" + "github.com/ava-labs/avalanche-cli/pkg/subnet" + "github.com/ava-labs/avalanche-cli/pkg/vm" + + "github.com/ava-labs/avalanche-cli/pkg/ansible" + + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ssh" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/api/info" +) + +const ( + HealthCheckPoolTime = 60 * time.Second + HealthCheckTimeout = 3 * time.Minute +) + +type AvalancheGoVersionSettings struct { + UseCustomAvalanchegoVersion string + UseLatestAvalanchegoReleaseVersion bool + UseLatestAvalanchegoPreReleaseVersion bool + UseAvalanchegoVersionFromSubnet string +} + +type ANRSettings struct { + GenesisPath string + UpgradePath string + BootstrapIDs []string + BootstrapIPs []string + StakingTLSKeyPath string + StakingCertKeyPath string + StakingSignerKeyPath string +} + +func AuthorizedAccessFromSettings(app *application.Avalanche) bool { + return app.Conf.GetConfigBoolValue(constants.ConfigAuthorizeCloudAccessKey) +} + +func CheckCluster(app *application.Avalanche, clusterName string) error { + _, err := GetClusterNodes(app, clusterName) + return err +} + +func GetClusterNodes(app *application.Avalanche, clusterName string) ([]string, error) { + if exists, err := CheckClusterExists(app, clusterName); err != nil || !exists { + return nil, fmt.Errorf("cluster %q not found", clusterName) + } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return nil, err + } + clusterNodes := clusterConfig.Nodes + if len(clusterNodes) == 0 && !clusterConfig.Local { + return nil, fmt.Errorf("no nodes found in cluster %s", clusterName) + } + return clusterNodes, nil +} + +func CheckClusterExists(app *application.Avalanche, clusterName string) (bool, error) { + clustersConfig := models.ClustersConfig{} + if app.ClustersConfigExists() { + var err error + clustersConfig, err = app.LoadClustersConfig() + if err != nil { + return false, err + } + } + _, ok := clustersConfig.Clusters[clusterName] + return ok, nil +} + +func CheckHostsAreRPCCompatible(app *application.Avalanche, hosts []*models.Host, subnetName string) error { + incompatibleNodes, err := getRPCIncompatibleNodes(app, hosts, subnetName) + if err != nil { + return err + } + if len(incompatibleNodes) > 0 { + sc, err := app.LoadSidecar(subnetName) + if err != nil { + return err + } + ux.Logger.PrintToUser("Either modify your Avalanche Go version or modify your VM version") + ux.Logger.PrintToUser("To modify your Avalanche Go version: https://docs.avax.network/nodes/maintain/upgrade-your-avalanchego-node") + switch sc.VM { + case models.SubnetEvm: + ux.Logger.PrintToUser("To modify your Subnet-EVM version: https://docs.avax.network/build/subnet/upgrade/upgrade-subnet-vm") + case models.CustomVM: + ux.Logger.PrintToUser("To modify your Custom VM binary: avalanche subnet upgrade vm %s --config", subnetName) + } + ux.Logger.PrintToUser("Yoy can use \"avalanche node upgrade\" to upgrade Avalanche Go and/or Subnet-EVM to their latest versions") + return fmt.Errorf("the Avalanche Go version of node(s) %s is incompatible with VM RPC version of %s", incompatibleNodes, subnetName) + } + return nil +} + +func getRPCIncompatibleNodes(app *application.Avalanche, hosts []*models.Host, subnetName string) ([]string, error) { + ux.Logger.PrintToUser("Checking compatibility of node(s) avalanche go RPC protocol version with Subnet EVM RPC of subnet %s ...", subnetName) + sc, err := app.LoadSidecar(subnetName) + if err != nil { + return nil, err + } + wg := sync.WaitGroup{} + wgResults := models.NodeResults{} + for _, host := range hosts { + wg.Add(1) + go func(nodeResults *models.NodeResults, host *models.Host) { + defer wg.Done() + if resp, err := ssh.RunSSHCheckAvalancheGoVersion(host); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + return + } else { + if _, rpcVersion, err := ParseAvalancheGoOutput(resp); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + } else { + nodeResults.AddResult(host.GetCloudID(), rpcVersion, err) + } + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return nil, fmt.Errorf("failed to get rpc protocol version for node(s) %s", wgResults.GetErrorHostMap()) + } + incompatibleNodes := []string{} + for nodeID, rpcVersionI := range wgResults.GetResultMap() { + rpcVersion := rpcVersionI.(uint32) + if rpcVersion != uint32(sc.RPCVersion) { + incompatibleNodes = append(incompatibleNodes, nodeID) + } + } + if len(incompatibleNodes) > 0 { + ux.Logger.PrintToUser(fmt.Sprintf("Compatible Avalanche Go RPC version is %d", sc.RPCVersion)) + } + return incompatibleNodes, nil +} + +func ParseAvalancheGoOutput(byteValue []byte) (string, uint32, error) { + reply := map[string]interface{}{} + if err := json.Unmarshal(byteValue, &reply); err != nil { + return "", 0, err + } + resultMap := reply["result"] + resultJSON, err := json.Marshal(resultMap) + if err != nil { + return "", 0, err + } + + nodeVersionReply := info.GetNodeVersionReply{} + if err := json.Unmarshal(resultJSON, &nodeVersionReply); err != nil { + return "", 0, err + } + return nodeVersionReply.VMVersions["platform"], uint32(nodeVersionReply.RPCProtocolVersion), nil +} + +func DisconnectHosts(hosts []*models.Host) { + for _, host := range hosts { + _ = host.Disconnect() + } +} + +func GetWSEndpoint(endpoint string, blockchainID string) string { + return models.NewDevnetNetwork(endpoint, 0).BlockchainWSEndpoint(blockchainID) +} + +func getPublicEndpoints( + app *application.Avalanche, + clusterName string, + trackers []*models.Host, +) ([]string, error) { + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return nil, err + } + publicNodes := clusterConfig.APINodes + if clusterConfig.Network.Kind == models.Devnet { + publicNodes = clusterConfig.Nodes + } + publicTrackers := utils.Filter(trackers, func(tracker *models.Host) bool { + return utils.Belongs(publicNodes, tracker.GetCloudID()) + }) + endpoints := utils.Map(publicTrackers, func(tracker *models.Host) string { + return GetAvalancheGoEndpoint(tracker.IP) + }) + return endpoints, nil +} + +func GetRPCEndpoint(endpoint string, blockchainID string) string { + return models.NewDevnetNetwork(endpoint, 0).BlockchainEndpoint(blockchainID) +} + +func GetAvalancheGoEndpoint(ip string) string { + return fmt.Sprintf("http://%s:%d", ip, constants.AvalanchegoAPIPort) +} + +func GetUnhealthyNodes(hosts []*models.Host) ([]string, error) { + wg := sync.WaitGroup{} + wgResults := models.NodeResults{} + for _, host := range hosts { + wg.Add(1) + go func(nodeResults *models.NodeResults, host *models.Host) { + defer wg.Done() + if resp, err := ssh.RunSSHCheckHealthy(host); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + return + } else { + if isHealthy, err := parseHealthyOutput(resp); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + } else { + nodeResults.AddResult(host.GetCloudID(), isHealthy, err) + } + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return nil, fmt.Errorf("failed to get health status for node(s) %s", wgResults.GetErrorHostMap()) + } + return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool { + return !wgResults.GetResultMap()[nodeID].(bool) + }), nil +} + +func parseHealthyOutput(byteValue []byte) (bool, error) { + var result map[string]interface{} + if err := json.Unmarshal(byteValue, &result); err != nil { + return false, err + } + isHealthyInterface, ok := result["result"].(map[string]interface{}) + if ok { + isHealthy, ok := isHealthyInterface["healthy"].(bool) + if ok { + return isHealthy, nil + } + } + return false, fmt.Errorf("unable to parse node healthy status") +} + +func WaitForHealthyCluster( + app *application.Avalanche, + clusterName string, + timeout time.Duration, + poolTime time.Duration, +) error { + ux.Logger.PrintToUser("") + ux.Logger.PrintToUser("Waiting for node(s) in cluster %s to be healthy...", clusterName) + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return err + } + cluster, ok := clustersConfig.Clusters[clusterName] + if !ok { + return fmt.Errorf("cluster %s does not exist", clusterName) + } + allHosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) + if err != nil { + return err + } + hosts := cluster.GetValidatorHosts(allHosts) // exlude api nodes + defer DisconnectHosts(hosts) + startTime := time.Now() + spinSession := ux.NewUserSpinner() + spinner := spinSession.SpinToUser("Checking if node(s) are healthy...") + for { + unhealthyNodes, err := GetUnhealthyNodes(hosts) + if err != nil { + ux.SpinFailWithError(spinner, "", err) + return err + } + if len(unhealthyNodes) == 0 { + ux.SpinComplete(spinner) + spinSession.Stop() + ux.Logger.GreenCheckmarkToUser("Nodes healthy after %d seconds", uint32(time.Since(startTime).Seconds())) + return nil + } + if time.Since(startTime) > timeout { + ux.SpinFailWithError(spinner, "", fmt.Errorf("cluster not healthy after %d seconds", uint32(timeout.Seconds()))) + spinSession.Stop() + ux.Logger.PrintToUser("") + ux.Logger.RedXToUser("Unhealthy Nodes") + for _, failedNode := range unhealthyNodes { + ux.Logger.PrintToUser(" " + failedNode) + } + ux.Logger.PrintToUser("") + return fmt.Errorf("cluster not healthy after %d seconds", uint32(timeout.Seconds())) + } + time.Sleep(poolTime) + } +} + +func GetClusterNameFromList(app *application.Avalanche) (string, error) { + clusterNames, err := app.ListClusterNames() + if err != nil { + return "", err + } + if len(clusterNames) == 0 { + return "", fmt.Errorf("no Avalanche nodes found that can track the blockchain, please create Avalanche nodes first through `avalanche node create`") + } + clusterName, err := app.Prompt.CaptureList( + "Which cluster of Avalanche nodes would you like to use to track the blockchain?", + clusterNames, + ) + if err != nil { + return "", err + } + return clusterName, nil +} + +// GetAvalancheGoVersion asks users whether they want to install the newest Avalanche Go version +// or if they want to use the newest Avalanche Go Version that is still compatible with Subnet EVM +// version of their choice +func GetAvalancheGoVersion(app *application.Avalanche, avagoVersion AvalancheGoVersionSettings) (string, error) { + // skip this logic if custom-avalanchego-version flag is set + if avagoVersion.UseCustomAvalanchegoVersion != "" { + return avagoVersion.UseCustomAvalanchegoVersion, nil + } + latestReleaseVersion, err := app.Downloader.GetLatestReleaseVersion(binutils.GetGithubLatestReleaseURL( + constants.AvaLabsOrg, + constants.AvalancheGoRepoName, + )) + if err != nil { + return "", err + } + latestPreReleaseVersion, err := app.Downloader.GetLatestPreReleaseVersion( + constants.AvaLabsOrg, + constants.AvalancheGoRepoName, + ) + if err != nil { + return "", err + } + + if !avagoVersion.UseLatestAvalanchegoReleaseVersion && !avagoVersion.UseLatestAvalanchegoPreReleaseVersion && avagoVersion.UseCustomAvalanchegoVersion == "" && avagoVersion.UseAvalanchegoVersionFromSubnet == "" { + avagoVersion, err = promptAvalancheGoVersionChoice(app, latestReleaseVersion, latestPreReleaseVersion) + if err != nil { + return "", err + } + } + + var version string + switch { + case avagoVersion.UseLatestAvalanchegoReleaseVersion: + version = latestReleaseVersion + case avagoVersion.UseLatestAvalanchegoPreReleaseVersion: + version = latestPreReleaseVersion + case avagoVersion.UseCustomAvalanchegoVersion != "": + version = avagoVersion.UseCustomAvalanchegoVersion + case avagoVersion.UseAvalanchegoVersionFromSubnet != "": + sc, err := app.LoadSidecar(avagoVersion.UseAvalanchegoVersionFromSubnet) + if err != nil { + return "", err + } + version, err = GetLatestAvagoVersionForRPC(app, sc.RPCVersion, latestPreReleaseVersion) + if err != nil { + return "", err + } + } + return version, nil +} + +// promptAvalancheGoVersionChoice sets flags for either using the latest Avalanche Go +// version or using the latest Avalanche Go version that is still compatible with the subnet that user +// wants the cloud server to track +func promptAvalancheGoVersionChoice(app *application.Avalanche, latestReleaseVersion string, latestPreReleaseVersion string) (AvalancheGoVersionSettings, error) { + versionComments := map[string]string{ + "v1.11.0-fuji": " (recommended for fuji durango)", + } + latestReleaseVersionOption := "Use latest Avalanche Go Release Version" + versionComments[latestReleaseVersion] + latestPreReleaseVersionOption := "Use latest Avalanche Go Pre-release Version" + versionComments[latestPreReleaseVersion] + subnetBasedVersionOption := "Use the deployed Subnet's VM version that the node will be validating" + customOption := "Custom" + + txt := "What version of Avalanche Go would you like to install in the node?" + versionOptions := []string{latestReleaseVersionOption, subnetBasedVersionOption, customOption} + if latestPreReleaseVersion != latestReleaseVersion { + versionOptions = []string{latestPreReleaseVersionOption, latestReleaseVersionOption, subnetBasedVersionOption, customOption} + } + versionOption, err := app.Prompt.CaptureList(txt, versionOptions) + if err != nil { + return AvalancheGoVersionSettings{}, err + } + + switch versionOption { + case latestReleaseVersionOption: + return AvalancheGoVersionSettings{UseLatestAvalanchegoReleaseVersion: true}, nil + case latestPreReleaseVersionOption: + return AvalancheGoVersionSettings{UseLatestAvalanchegoPreReleaseVersion: true}, nil + case customOption: + useCustomAvalanchegoVersion, err := app.Prompt.CaptureVersion("Which version of AvalancheGo would you like to install? (Use format v1.10.13)") + if err != nil { + return AvalancheGoVersionSettings{}, err + } + return AvalancheGoVersionSettings{UseCustomAvalanchegoVersion: useCustomAvalanchegoVersion}, nil + default: + useAvalanchegoVersionFromSubnet := "" + for { + useAvalanchegoVersionFromSubnet, err = app.Prompt.CaptureString("Which Subnet would you like to use to choose the avalanche go version?") + if err != nil { + return AvalancheGoVersionSettings{}, err + } + _, err = subnet.ValidateSubnetNameAndGetChains(app, []string{useAvalanchegoVersionFromSubnet}) + if err == nil { + break + } + ux.Logger.PrintToUser(fmt.Sprintf("no subnet named %s found", useAvalanchegoVersionFromSubnet)) + } + return AvalancheGoVersionSettings{UseAvalanchegoVersionFromSubnet: useAvalanchegoVersionFromSubnet}, nil + } +} + +func GetLatestAvagoVersionForRPC(app *application.Avalanche, configuredRPCVersion int, latestPreReleaseVersion string) (string, error) { + desiredAvagoVersion, err := vm.GetLatestAvalancheGoByProtocolVersion( + app, configuredRPCVersion, constants.AvalancheGoCompatibilityURL) + if errors.Is(err, vm.ErrNoAvagoVersion) { + ux.Logger.PrintToUser("No Avago version found for subnet. Defaulting to latest pre-release version") + return latestPreReleaseVersion, nil + } + if err != nil { + return "", err + } + return desiredAvagoVersion, nil +} diff --git a/pkg/node/local.go b/pkg/node/local.go new file mode 100644 index 000000000..49323fd68 --- /dev/null +++ b/pkg/node/local.go @@ -0,0 +1,690 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/binutils" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/localnet" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/networkoptions" + "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/avalanche-network-runner/client" + anrutils "github.com/ava-labs/avalanche-network-runner/utils" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + avagoconstants "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" +) + +func TrackSubnetWithLocalMachine( + app *application.Avalanche, + clusterName, + blockchainName string, + avalancheGoBinPath string, +) error { + if ok, err := checkClusterIsLocal(app, clusterName); err != nil || !ok { + return fmt.Errorf("local node %q is not found", clusterName) + } + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return err + } + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return err + } + clusterConfig := clustersConfig.Clusters[clusterName] + network := models.GetNetworkFromCluster(clusterConfig) + if network.ClusterName != "" { + network = models.ConvertClusterToNetwork(network) + } + if sc.Networks[network.Name()].BlockchainID == ids.Empty { + return fmt.Errorf("blockchain %s has not been deployed to %s", blockchainName, network.Name()) + } + subnetID := sc.Networks[network.Name()].SubnetID + blockchainID := sc.Networks[network.Name()].BlockchainID + vmID, err := anrutils.VMID(blockchainName) + if err != nil { + return fmt.Errorf("failed to create VM ID from %s: %w", blockchainName, err) + } + var vmBin string + switch sc.VM { + case models.SubnetEvm: + _, vmBin, err = binutils.SetupSubnetEVM(app, sc.VMVersion) + if err != nil { + return fmt.Errorf("failed to install subnet-evm: %w", err) + } + case models.CustomVM: + vmBin = binutils.SetupCustomBin(app, blockchainName) + default: + return fmt.Errorf("unknown vm: %s", sc.VM) + } + rootDir := app.GetLocalDir(clusterName) + pluginPath := filepath.Join(rootDir, "node1", "plugins", vmID.String()) + if err := utils.FileCopy(vmBin, pluginPath); err != nil { + return err + } + if err := os.Chmod(pluginPath, constants.DefaultPerms755); err != nil { + return err + } + + cli, err := binutils.NewGRPCClientWithEndpoint( + binutils.LocalClusterGRPCServerEndpoint, + binutils.WithAvoidRPCVersionCheck(true), + binutils.WithDialTimeout(constants.FastGRPCDialTimeout), + ) + if err != nil { + return err + } + ctx, cancel := utils.GetANRContext() + defer cancel() + status, err := cli.Status(ctx) + if err != nil { + return err + } + publicEndpoints := []string{} + for _, nodeInfo := range status.ClusterInfo.NodeInfos { + if app.ChainConfigExists(blockchainName) { + inputChainConfigPath := app.GetChainConfigPath(blockchainName) + outputChainConfigPath := filepath.Join(rootDir, nodeInfo.Name, "configs", "chains", blockchainID.String(), "config.json") + if err := os.MkdirAll(filepath.Dir(outputChainConfigPath), 0o700); err != nil { + return fmt.Errorf("could not create chain conf directory %s: %w", filepath.Dir(outputChainConfigPath), err) + } + if err := utils.FileCopy(inputChainConfigPath, outputChainConfigPath); err != nil { + return err + } + } + ux.Logger.PrintToUser("Restarting node %s to track subnet", nodeInfo.Name) + opts := []client.OpOption{ + client.WithWhitelistedSubnets(subnetID.String()), + client.WithExecPath(avalancheGoBinPath), + } + if _, err := cli.RestartNode(ctx, nodeInfo.Name, opts...); err != nil { + return err + } + publicEndpoints = append(publicEndpoints, nodeInfo.Uri) + } + networkInfo := sc.Networks[network.Name()] + rpcEndpoints := set.Of(networkInfo.RPCEndpoints...) + wsEndpoints := set.Of(networkInfo.WSEndpoints...) + for _, publicEndpoint := range publicEndpoints { + rpcEndpoints.Add(GetRPCEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) + wsEndpoints.Add(GetWSEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) + } + networkInfo.RPCEndpoints = rpcEndpoints.List() + networkInfo.WSEndpoints = wsEndpoints.List() + for _, rpcURL := range networkInfo.RPCEndpoints { + ux.Logger.PrintToUser("Waiting for rpc %s to be available", rpcURL) + if err := evm.WaitForRPC(ctx, rpcURL); err != nil { + return err + } + } + sc.Networks[network.Name()] = networkInfo + if err := app.UpdateSidecar(&sc); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("%s successfully tracking %s", clusterName, blockchainName) + return nil +} + +func checkClusterIsLocal(app *application.Avalanche, clusterName string) (bool, error) { + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return false, err + } + clusterConf, ok := clustersConfig.Clusters[clusterName] + return ok && clusterConf.Local, nil +} + +func StartLocalNode( + app *application.Avalanche, + clusterName string, + useEtnaDevnet bool, + avalanchegoBinaryPath string, + numNodes uint32, + partialSync bool, + nodeConfig map[string]interface{}, + anrSettings ANRSettings, + avaGoVersionSetting AvalancheGoVersionSettings, + globalNetworkFlags networkoptions.NetworkFlags, + createSupportedNetworkOptions []networkoptions.NetworkOption, +) error { + var err error + + // ensure data consistency + localClusterExists := false + if clusterExists, err := CheckClusterExists(app, clusterName); err != nil { + return fmt.Errorf("error checking clusters info: %w", err) + } else if clusterExists { + if localClusterExists, err = checkClusterIsLocal(app, clusterName); err != nil { + return fmt.Errorf("error verifying if cluster is local: %w", err) + } else if !localClusterExists { + return fmt.Errorf("cluster %s is not a local one", clusterName) + } + } + localDataExists := localClusterDataExists(app, clusterName) + if (localClusterExists && !localDataExists) || (!localClusterExists && localDataExists) { + ux.Logger.RedXToUser("Inconsistent state for cluster: Cleaning up") + _ = DestroyLocalNode(app, clusterName) + localClusterExists = false + localDataExists = false + } + + // check if this is existing cluster + rootDir := app.GetLocalDir(clusterName) + pluginDir := filepath.Join(rootDir, "node1", "plugins") + // make sure rootDir exists + if err := os.MkdirAll(rootDir, 0o700); err != nil { + return fmt.Errorf("could not create root directory %s: %w", rootDir, err) + } + // make sure pluginDir exists + if err := os.MkdirAll(pluginDir, 0o700); err != nil { + return fmt.Errorf("could not create plugin directory %s: %w", pluginDir, err) + } + + ctx, cancel := utils.GetANRContext() + defer cancel() + + // starts server + avalancheGoVersion := "latest" + if avalanchegoBinaryPath == "" { + avalancheGoVersion, err = GetAvalancheGoVersion(app, avaGoVersionSetting) + if err != nil { + return err + } + _, avagoDir, err := binutils.SetupAvalanchego(app, avalancheGoVersion) + if err != nil { + return fmt.Errorf("failed installing Avalanche Go version %s: %w", avalancheGoVersion, err) + } + avalanchegoBinaryPath = filepath.Join(avagoDir, "avalanchego") + ux.Logger.PrintToUser("Using AvalancheGo version: %s", avalancheGoVersion) + } + serverLogPath := filepath.Join(rootDir, "server.log") + sd := subnet.NewLocalDeployer(app, avalancheGoVersion, avalanchegoBinaryPath, "", true) + if err := sd.StartServer( + constants.ServerRunFileLocalClusterPrefix, + binutils.LocalClusterGRPCServerPort, + binutils.LocalClusterGRPCGatewayPort, + rootDir, + serverLogPath, + ); err != nil { + return err + } + _, avalancheGoBinPath, err := sd.SetupLocalEnv() + if err != nil { + return err + } + cli, err := binutils.NewGRPCClientWithEndpoint(binutils.LocalClusterGRPCServerEndpoint) + if err != nil { + return err + } + alreadyBootstrapped, err := localnet.CheckNetworkIsAlreadyBootstrapped(ctx, cli) + if err != nil { + return err + } + if alreadyBootstrapped { + ux.Logger.PrintToUser("") + ux.Logger.PrintToUser("A local cluster is already executing") + ux.Logger.PrintToUser("please stop it by calling 'node local stop'") + return nil + } + + if nodeConfig == nil { + nodeConfig = map[string]interface{}{} + } + if partialSync { + nodeConfig[config.PartialSyncPrimaryNetworkKey] = true + } + + nodeConfigBytes, err := json.Marshal(nodeConfig) + if err != nil { + return err + } + nodeConfigStr := string(nodeConfigBytes) + if localClusterExists && localDataExists { + ux.Logger.GreenCheckmarkToUser("Local cluster %s found. Booting up...", clusterName) + loadSnapshotOpts := []client.OpOption{ + client.WithExecPath(avalancheGoBinPath), + client.WithReassignPortsIfUsed(true), + client.WithPluginDir(pluginDir), + client.WithSnapshotPath(rootDir), + client.WithGlobalNodeConfig(nodeConfigStr), + } + // load snapshot for existing network + if _, err := cli.LoadSnapshot( + ctx, + clusterName, + true, // in-place + loadSnapshotOpts..., + ); err != nil { + return fmt.Errorf("failed to load snapshot: %w", err) + } + } else { + ux.Logger.GreenCheckmarkToUser("Local cluster %s not found. Creating...", clusterName) + network := models.UndefinedNetwork + switch { + case useEtnaDevnet: + network = models.NewNetwork( + models.Devnet, + constants.EtnaDevnetNetworkID, + constants.EtnaDevnetEndpoint, + clusterName, + ) + case globalNetworkFlags.UseFuji: + network = models.NewNetwork( + models.Fuji, + avagoconstants.FujiID, + constants.FujiAPIEndpoint, + clusterName, + ) + default: + network, err = networkoptions.GetNetworkFromCmdLineFlags( + app, + "", + globalNetworkFlags, + false, + true, + createSupportedNetworkOptions, + "", + ) + if err != nil { + return err + } + } + if err := preLocalChecks(anrSettings, avaGoVersionSetting, useEtnaDevnet, globalNetworkFlags); err != nil { + return err + } + if useEtnaDevnet { + anrSettings.BootstrapIDs = constants.EtnaDevnetBootstrapNodeIDs + anrSettings.BootstrapIPs = constants.EtnaDevnetBootstrapIPs + // prepare genesis and upgrade files for anr + genesisFile, err := os.CreateTemp("", "etna_devnet_genesis") + if err != nil { + return fmt.Errorf("could not create save Etna Devnet genesis file: %w", err) + } + if _, err := genesisFile.Write(constants.EtnaDevnetGenesisData); err != nil { + return fmt.Errorf("could not write Etna Devnet genesis data: %w", err) + } + if err := genesisFile.Close(); err != nil { + return fmt.Errorf("could not close Etna Devnet genesis file: %w", err) + } + anrSettings.GenesisPath = genesisFile.Name() + defer os.Remove(anrSettings.GenesisPath) + + upgradeFile, err := os.CreateTemp("", "etna_devnet_upgrade") + if err != nil { + return fmt.Errorf("could not create save Etna Devnet upgrade file: %w", err) + } + if _, err := upgradeFile.Write(constants.EtnaDevnetUpgradeData); err != nil { + return fmt.Errorf("could not write Etna Devnet upgrade data: %w", err) + } + anrSettings.UpgradePath = upgradeFile.Name() + if err := upgradeFile.Close(); err != nil { + return fmt.Errorf("could not close Etna Devnet upgrade file: %w", err) + } + defer os.Remove(anrSettings.UpgradePath) + } + + if anrSettings.StakingTLSKeyPath != "" && anrSettings.StakingCertKeyPath != "" && anrSettings.StakingSignerKeyPath != "" { + if err := os.MkdirAll(filepath.Join(rootDir, "node1", "staking"), 0o700); err != nil { + return fmt.Errorf("could not create root directory %s: %w", rootDir, err) + } + if err := utils.FileCopy(anrSettings.StakingTLSKeyPath, filepath.Join(rootDir, "node1", "staking", "staker.key")); err != nil { + return err + } + if err := utils.FileCopy(anrSettings.StakingCertKeyPath, filepath.Join(rootDir, "node1", "staking", "staker.crt")); err != nil { + return err + } + if err := utils.FileCopy(anrSettings.StakingSignerKeyPath, filepath.Join(rootDir, "node1", "staking", "signer.key")); err != nil { + return err + } + } + + anrOpts := []client.OpOption{ + client.WithNumNodes(numNodes), + client.WithNetworkID(network.ID), + client.WithExecPath(avalancheGoBinPath), + client.WithRootDataDir(rootDir), + client.WithReassignPortsIfUsed(true), + client.WithPluginDir(pluginDir), + client.WithFreshStakingIds(true), + client.WithZeroIP(false), + client.WithGlobalNodeConfig(nodeConfigStr), + } + if anrSettings.GenesisPath != "" && utils.FileExists(anrSettings.GenesisPath) { + anrOpts = append(anrOpts, client.WithGenesisPath(anrSettings.GenesisPath)) + } + if anrSettings.UpgradePath != "" && utils.FileExists(anrSettings.UpgradePath) { + anrOpts = append(anrOpts, client.WithUpgradePath(anrSettings.UpgradePath)) + } + if anrSettings.BootstrapIDs != nil { + anrOpts = append(anrOpts, client.WithBootstrapNodeIDs(anrSettings.BootstrapIDs)) + } + if anrSettings.BootstrapIPs != nil { + anrOpts = append(anrOpts, client.WithBootstrapNodeIPPortPairs(anrSettings.BootstrapIPs)) + } + + ctx, cancel = network.BootstrappingContext() + defer cancel() + + ux.Logger.PrintToUser("Starting local avalanchego node using root: %s ...", rootDir) + spinSession := ux.NewUserSpinner() + spinner := spinSession.SpinToUser("Booting Network. Wait until healthy...") + if _, err := cli.Start(ctx, avalancheGoBinPath, anrOpts...); err != nil { + ux.SpinFailWithError(spinner, "", err) + _ = DestroyLocalNode(app, clusterName) + return fmt.Errorf("failed to start local avalanchego: %w", err) + } + ux.SpinComplete(spinner) + spinSession.Stop() + // save cluster config for the new local cluster + if err := addLocalClusterConfig(app, network); err != nil { + return err + } + } + + ux.Logger.GreenCheckmarkToUser("Avalanchego started and ready to use from %s", rootDir) + ux.Logger.PrintToUser("") + ux.Logger.PrintToUser("Node logs directory: %s/node1/logs", rootDir) + ux.Logger.PrintToUser("") + ux.Logger.PrintToUser("Network ready to use.") + ux.Logger.PrintToUser("") + + status, err := cli.Status(ctx) + if err != nil { + return err + } + for _, nodeInfo := range status.ClusterInfo.NodeInfos { + ux.Logger.PrintToUser("URI: %s", nodeInfo.Uri) + ux.Logger.PrintToUser("NodeID: %s", nodeInfo.Id) + ux.Logger.PrintToUser("") + } + + return nil +} + +func localClusterDataExists(app *application.Avalanche, clusterName string) bool { + rootDir := app.GetLocalDir(clusterName) + return utils.FileExists(filepath.Join(rootDir, "state.json")) +} + +// stub for now +func preLocalChecks(anrSettings ANRSettings, avaGoVersionSettings AvalancheGoVersionSettings, useEtnaDevnet bool, globalNetworkFlags networkoptions.NetworkFlags) error { + // expand passed paths + if anrSettings.GenesisPath != "" { + anrSettings.GenesisPath = utils.ExpandHome(anrSettings.GenesisPath) + } + if anrSettings.UpgradePath != "" { + anrSettings.UpgradePath = utils.ExpandHome(anrSettings.UpgradePath) + } + // checks + if avaGoVersionSettings.UseCustomAvalanchegoVersion != "" && (avaGoVersionSettings.UseLatestAvalanchegoReleaseVersion || avaGoVersionSettings.UseLatestAvalanchegoPreReleaseVersion) { + return fmt.Errorf("specify either --custom-avalanchego-version or --latest-avalanchego-version") + } + if useEtnaDevnet && (globalNetworkFlags.UseDevnet || globalNetworkFlags.UseFuji) { + return fmt.Errorf("etna devnet can only be used with devnet") + } + if useEtnaDevnet && anrSettings.GenesisPath != "" { + return fmt.Errorf("etna devnet uses predefined genesis file") + } + if useEtnaDevnet && anrSettings.UpgradePath != "" { + return fmt.Errorf("etna devnet uses predefined upgrade file") + } + if useEtnaDevnet && (len(anrSettings.BootstrapIDs) != 0 || len(anrSettings.BootstrapIPs) != 0) { + return fmt.Errorf("etna devnet uses predefined bootstrap configuration") + } + if len(anrSettings.BootstrapIDs) != len(anrSettings.BootstrapIPs) { + return fmt.Errorf("number of bootstrap IDs and bootstrap IP:port pairs must be equal") + } + if anrSettings.GenesisPath != "" && !utils.FileExists(anrSettings.GenesisPath) { + return fmt.Errorf("genesis file %s does not exist", anrSettings.GenesisPath) + } + if anrSettings.UpgradePath != "" && !utils.FileExists(anrSettings.UpgradePath) { + return fmt.Errorf("upgrade file %s does not exist", anrSettings.UpgradePath) + } + return nil +} + +func addLocalClusterConfig(app *application.Avalanche, network models.Network) error { + clusterName := network.ClusterName + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return err + } + clusterConfig := clustersConfig.Clusters[clusterName] + clusterConfig.Local = true + clusterConfig.Network = network + clustersConfig.Clusters[clusterName] = clusterConfig + return app.WriteClustersConfigFile(&clustersConfig) +} + +func DestroyLocalNode(app *application.Avalanche, clusterName string) error { + _ = StopLocalNode(app) + + rootDir := app.GetLocalDir(clusterName) + if err := os.RemoveAll(rootDir); err != nil { + return err + } + + if ok, err := checkClusterIsLocal(app, clusterName); err != nil || !ok { + return fmt.Errorf("local cluster %q not found", clusterName) + } + + clustersConfig, err := app.LoadClustersConfig() + if err != nil { + return err + } + delete(clustersConfig.Clusters, clusterName) + if err := app.WriteClustersConfigFile(&clustersConfig); err != nil { + return err + } + + ux.Logger.GreenCheckmarkToUser("Local node %s cleaned up.", clusterName) + return nil +} + +func StopLocalNode(app *application.Avalanche) error { + cli, err := binutils.NewGRPCClientWithEndpoint( + binutils.LocalClusterGRPCServerEndpoint, + binutils.WithAvoidRPCVersionCheck(true), + binutils.WithDialTimeout(constants.FastGRPCDialTimeout), + ) + if err != nil { + return err + } + ctx, cancel := utils.GetANRContext() + defer cancel() + bootstrapped, err := localnet.CheckNetworkIsAlreadyBootstrapped(ctx, cli) + if err != nil { + return err + } + if bootstrapped { + if _, err = cli.Stop(ctx); err != nil { + return fmt.Errorf("failed to stop avalanchego: %w", err) + } + } + if err := binutils.KillgRPCServerProcess( + app, + binutils.LocalClusterGRPCServerEndpoint, + constants.ServerRunFileLocalClusterPrefix, + ); err != nil { + return err + } + ux.Logger.GreenCheckmarkToUser("avalanchego stopped") + return nil +} + +func listLocalClusters(app *application.Avalanche, clusterNamesToInclude []string) (map[string]string, error) { + localClusters := map[string]string{} // map[clusterName]rootDir + clustersConfig, err := app.GetClustersConfig() + if err != nil { + return localClusters, err + } + for clusterName := range clustersConfig.Clusters { + if len(clusterNamesToInclude) == 0 || slices.Contains(clusterNamesToInclude, clusterName) { + if ok, err := checkClusterIsLocal(app, clusterName); err == nil && ok { + localClusters[clusterName] = app.GetLocalDir(clusterName) + } + } + } + return localClusters, nil +} + +func LocalStatus(app *application.Avalanche, clusterName string, blockchainName string) error { + clustersToList := make([]string, 0) + if clusterName != "" { + if ok, err := checkClusterIsLocal(app, clusterName); err != nil || !ok { + return fmt.Errorf("local cluster %q not found", clusterName) + } + clustersToList = append(clustersToList, clusterName) + } + + // get currently running local cluster + ctx, cancel := utils.GetANRContext() + defer cancel() + currentlyRunningRootDir := "" + isHealthy := false + cli, _ := binutils.NewGRPCClientWithEndpoint( // ignore error as ANR might be not running + binutils.LocalClusterGRPCServerEndpoint, + binutils.WithAvoidRPCVersionCheck(true), + binutils.WithDialTimeout(constants.FastGRPCDialTimeout), + ) + runningAvagoURIs := []string{} + if cli != nil { + status, _ := cli.Status(ctx) // ignore error as ANR might be not running + if status != nil && status.ClusterInfo != nil { + if status.ClusterInfo.RootDataDir != "" { + currentlyRunningRootDir = status.ClusterInfo.RootDataDir + } + isHealthy = status.ClusterInfo.Healthy + // get list of the nodes + for _, nodeInfo := range status.ClusterInfo.NodeInfos { + runningAvagoURIs = append(runningAvagoURIs, nodeInfo.Uri) + } + } + } + localClusters, err := listLocalClusters(app, clustersToList) + if err != nil { + return fmt.Errorf("failed to list local clusters: %w", err) + } + if clusterName != "" { + ux.Logger.PrintToUser("%s %s", logging.Blue.Wrap("Local cluster:"), logging.Green.Wrap(clusterName)) + } else { + ux.Logger.PrintToUser(logging.Blue.Wrap("Local clusters:")) + } + for clusterName, rootDir := range localClusters { + currenlyRunning := "" + healthStatus := "" + avagoURIOuput := "" + + // load sidecar and cluster config for the cluster if blockchainName is not empty + blockchainID := ids.Empty + if blockchainName != "" { + clusterConf, err := app.GetClusterConfig(clusterName) + if err != nil { + return fmt.Errorf("failed to get cluster config: %w", err) + } + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return err + } + network := models.ConvertClusterToNetwork(clusterConf.Network) + blockchainID = sc.Networks[network.Name()].BlockchainID + } + if rootDir == currentlyRunningRootDir { + currenlyRunning = fmt.Sprintf(" [%s]", logging.Blue.Wrap("Running")) + if isHealthy { + healthStatus = fmt.Sprintf(" [%s]", logging.Green.Wrap("Healthy")) + } else { + healthStatus = fmt.Sprintf(" [%s]", logging.Red.Wrap("Unhealthy")) + } + for _, avagoURI := range runningAvagoURIs { + nodeID, nodePOP, isBoot, err := GetInfo(avagoURI, blockchainID.String()) + if err != nil { + ux.Logger.RedXToUser("failed to get node %s info: %v", avagoURI, err) + continue + } + nodePOPPubKey := "0x" + hex.EncodeToString(nodePOP.PublicKey[:]) + nodePOPProof := "0x" + hex.EncodeToString(nodePOP.ProofOfPossession[:]) + + isBootStr := "Primary:" + logging.Red.Wrap("Not Bootstrapped") + if isBoot { + isBootStr = "Primary:" + logging.Green.Wrap("Bootstrapped") + } + + blockchainStatus := "" + if blockchainID != ids.Empty { + blockchainStatus, _ = GetBlockchainStatus(avagoURI, blockchainID.String()) // silence errors + } + + avagoURIOuput += fmt.Sprintf(" - %s [%s] [%s]\n publicKey: %s \n proofOfPossession: %s \n", + logging.LightBlue.Wrap(avagoURI), + nodeID, + strings.TrimRight(strings.Join([]string{isBootStr, "L1:" + logging.Orange.Wrap(blockchainStatus)}, " "), " "), + nodePOPPubKey, + nodePOPProof, + ) + } + } else { + currenlyRunning = fmt.Sprintf(" [%s]", logging.Black.Wrap("Stopped")) + } + ux.Logger.PrintToUser("- %s: %s %s %s", clusterName, rootDir, currenlyRunning, healthStatus) + ux.Logger.PrintToUser(avagoURIOuput) + } + + return nil +} + +func GetInfo(uri string, blockchainID string) ( + ids.NodeID, // nodeID + *signer.ProofOfPossession, // nodePOP + bool, // isBootstrapped + error, // error +) { + client := info.NewClient(uri) + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + nodeID, nodePOP, err := client.GetNodeID(ctx) + if err != nil { + return ids.EmptyNodeID, &signer.ProofOfPossession{}, false, err + } + isBootstrapped, err := client.IsBootstrapped(ctx, blockchainID) + if err != nil { + return nodeID, nodePOP, isBootstrapped, err + } + return nodeID, nodePOP, isBootstrapped, err +} + +func GetBlockchainStatus(uri string, blockchainID string) ( + string, // status + error, // error +) { + client := platformvm.NewClient(uri) + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + status, err := client.GetBlockchainStatus(ctx, blockchainID) + if err != nil { + return "", err + } + if status.String() == "" { + return "Not Syncing", nil + } + return status.String(), nil +} diff --git a/pkg/node/sync.go b/pkg/node/sync.go new file mode 100644 index 000000000..106a3e7f1 --- /dev/null +++ b/pkg/node/sync.go @@ -0,0 +1,235 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package node + +import ( + "encoding/json" + "errors" + "fmt" + "sync" + + "github.com/ava-labs/avalanche-cli/pkg/ansible" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ssh" + "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/utils/set" +) + +func SyncSubnet(app *application.Avalanche, clusterName, blockchainName string, avoidChecks bool, subnetAliases []string) error { + if err := CheckCluster(app, clusterName); err != nil { + return err + } + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + if _, err := subnet.ValidateSubnetNameAndGetChains(app, []string{blockchainName}); err != nil { + return err + } + hosts, err := ansible.GetInventoryFromAnsibleInventoryFile(app.GetAnsibleInventoryDirPath(clusterName)) + if err != nil { + return err + } + defer DisconnectHosts(hosts) + if !avoidChecks { + if err := CheckHostsAreBootstrapped(hosts); err != nil { + return err + } + if err := CheckHostsAreHealthy(hosts); err != nil { + return err + } + if err := CheckHostsAreRPCCompatible(app, hosts, blockchainName); err != nil { + return err + } + } + if err := prepareSubnetPlugin(app, hosts, blockchainName); err != nil { + return err + } + if err := trackSubnet(app, hosts, clusterName, clusterConfig.Network, blockchainName, subnetAliases); err != nil { + return err + } + ux.Logger.PrintToUser("Node(s) successfully started syncing with blockchain!") + ux.Logger.PrintToUser(fmt.Sprintf("Check node blockchain syncing status with avalanche node status %s --blockchain %s", clusterName, blockchainName)) + return nil +} + +// prepareSubnetPlugin creates subnet plugin to all nodes in the cluster +func prepareSubnetPlugin(app *application.Avalanche, hosts []*models.Host, blockchainName string) error { + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return err + } + wg := sync.WaitGroup{} + wgResults := models.NodeResults{} + for _, host := range hosts { + wg.Add(1) + go func(nodeResults *models.NodeResults, host *models.Host) { + defer wg.Done() + if err := ssh.RunSSHCreatePlugin(host, sc); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return fmt.Errorf("failed to upload plugin to node(s) %s", wgResults.GetErrorHostMap()) + } + return nil +} + +func trackSubnet( + app *application.Avalanche, + hosts []*models.Host, + clusterName string, + network models.Network, + blockchainName string, + subnetAliases []string, +) error { + // load cluster config + clusterConfig, err := app.GetClusterConfig(clusterName) + if err != nil { + return err + } + // and get list of subnets + allSubnets := utils.Unique(append(clusterConfig.Subnets, blockchainName)) + + // load sidecar to get subnet blockchain ID + sc, err := app.LoadSidecar(blockchainName) + if err != nil { + return err + } + blockchainID := sc.Networks[network.Name()].BlockchainID + + wg := sync.WaitGroup{} + wgResults := models.NodeResults{} + for _, host := range hosts { + wg.Add(1) + go func(nodeResults *models.NodeResults, host *models.Host) { + defer wg.Done() + if err := ssh.RunSSHStopNode(host); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + + if err := ssh.RunSSHRenderAvagoAliasConfigFile( + host, + blockchainID.String(), + subnetAliases, + ); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHRenderAvalancheNodeConfig( + app, + host, + network, + allSubnets, + clusterConfig.IsAPIHost(host.GetCloudID()), + ); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHSyncSubnetData(app, host, network, blockchainName); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + } + if err := ssh.RunSSHStartNode(host); err != nil { + nodeResults.AddResult(host.NodeID, nil, err) + return + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return fmt.Errorf("failed to track subnet for node(s) %s", wgResults.GetErrorHostMap()) + } + + // update slice of subnets synced by the cluster + clusterConfig.Subnets = allSubnets + err = app.SetClusterConfig(network.ClusterName, clusterConfig) + if err != nil { + return err + } + + // update slice of blockchain endpoints with the cluster ones + networkInfo := sc.Networks[clusterConfig.Network.Name()] + rpcEndpoints := set.Of(networkInfo.RPCEndpoints...) + wsEndpoints := set.Of(networkInfo.WSEndpoints...) + publicEndpoints, err := getPublicEndpoints(app, clusterName, hosts) + if err != nil { + return err + } + for _, publicEndpoint := range publicEndpoints { + rpcEndpoints.Add(GetRPCEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) + wsEndpoints.Add(GetWSEndpoint(publicEndpoint, networkInfo.BlockchainID.String())) + } + networkInfo.RPCEndpoints = rpcEndpoints.List() + networkInfo.WSEndpoints = wsEndpoints.List() + sc.Networks[clusterConfig.Network.Name()] = networkInfo + return app.UpdateSidecar(&sc) +} + +func CheckHostsAreBootstrapped(hosts []*models.Host) error { + notBootstrappedNodes, err := GetNotBootstrappedNodes(hosts) + if err != nil { + return err + } + if len(notBootstrappedNodes) > 0 { + return fmt.Errorf("node(s) %s are not bootstrapped yet, please try again later", notBootstrappedNodes) + } + return nil +} + +func CheckHostsAreHealthy(hosts []*models.Host) error { + ux.Logger.PrintToUser("Checking if node(s) are healthy...") + unhealthyNodes, err := GetUnhealthyNodes(hosts) + if err != nil { + return err + } + if len(unhealthyNodes) > 0 { + return fmt.Errorf("node(s) %s are not healthy, please check the issue and try again later", unhealthyNodes) + } + return nil +} + +func GetNotBootstrappedNodes(hosts []*models.Host) ([]string, error) { + wg := sync.WaitGroup{} + wgResults := models.NodeResults{} + for _, host := range hosts { + wg.Add(1) + go func(nodeResults *models.NodeResults, host *models.Host) { + defer wg.Done() + if resp, err := ssh.RunSSHCheckBootstrapped(host); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + return + } else { + if isBootstrapped, err := parseBootstrappedOutput(resp); err != nil { + nodeResults.AddResult(host.GetCloudID(), nil, err) + } else { + nodeResults.AddResult(host.GetCloudID(), isBootstrapped, err) + } + } + }(&wgResults, host) + } + wg.Wait() + if wgResults.HasErrors() { + return nil, fmt.Errorf("failed to get avalanchego bootstrap status for node(s) %s", wgResults.GetErrorHostMap()) + } + return utils.Filter(wgResults.GetNodeList(), func(nodeID string) bool { + return !wgResults.GetResultMap()[nodeID].(bool) + }), nil +} + +func parseBootstrappedOutput(byteValue []byte) (bool, error) { + var result map[string]interface{} + if err := json.Unmarshal(byteValue, &result); err != nil { + return false, err + } + isBootstrappedInterface, ok := result["result"].(map[string]interface{}) + if ok { + isBootstrapped, ok := isBootstrappedInterface["isBootstrapped"].(bool) + if ok { + return isBootstrapped, nil + } + } + return false, errors.New("unable to parse node bootstrap status") +} diff --git a/pkg/precompiles/allowlist.go b/pkg/precompiles/allowlist.go index b5fbc8e4d..7f8624285 100644 --- a/pkg/precompiles/allowlist.go +++ b/pkg/precompiles/allowlist.go @@ -22,6 +22,8 @@ func SetAdmin( privateKey, precompile, nil, + "set precompile admin", + nil, "setAdmin(address)", toSet, ) @@ -39,6 +41,8 @@ func SetManager( privateKey, precompile, nil, + "set precompile manager", + nil, "setManager(address)", toSet, ) @@ -56,6 +60,8 @@ func SetEnabled( privateKey, precompile, nil, + "set precompile enabled", + nil, "setEnabled(address)", toSet, ) @@ -73,6 +79,8 @@ func SetNone( privateKey, precompile, nil, + "set precompile none", + nil, "setNone(address)", toSet, ) diff --git a/pkg/prompts/prompts.go b/pkg/prompts/prompts.go index 028ad19c7..987e997fe 100644 --- a/pkg/prompts/prompts.go +++ b/pkg/prompts/prompts.go @@ -101,14 +101,19 @@ type Prompter interface { CaptureEmail(promptStr string) (string, error) CaptureIndex(promptStr string, options []any) (int, error) CaptureVersion(promptStr string) (string, error) + CaptureDuration(promptStr string) (time.Duration, error) + CaptureEtnaDuration(promptStr string) (time.Duration, error) CaptureFujiDuration(promptStr string) (time.Duration, error) CaptureMainnetDuration(promptStr string) (time.Duration, error) CaptureDate(promptStr string) (time.Time, error) CaptureNodeID(promptStr string) (ids.NodeID, error) CaptureID(promptStr string) (ids.ID, error) CaptureWeight(promptStr string) (uint64, error) + CaptureValidatorBalance(promptStr string, availableBalance uint64) (uint64, error) CapturePositiveInt(promptStr string, comparators []Comparator) (int, error) - CaptureInt(promptStr string) (int, error) + CaptureInt(promptStr string, validator func(int) error) (int, error) + CaptureUint8(promptStr string) (uint8, error) + CaptureUint16(promptStr string) (uint16, error) CaptureUint32(promptStr string) (uint32, error) CaptureUint64(promptStr string) (uint64, error) CaptureFloat(promptStr string, validator func(float64) error) (float64, error) @@ -200,6 +205,34 @@ func CaptureListDecision[T comparable]( } } +func (*realPrompter) CaptureEtnaDuration(promptStr string) (time.Duration, error) { + prompt := promptui.Prompt{ + Label: promptStr, + Validate: validateEtnaDuration, + } + + durationStr, err := prompt.Run() + if err != nil { + return 0, err + } + + return time.ParseDuration(durationStr) +} + +func (*realPrompter) CaptureDuration(promptStr string) (time.Duration, error) { + prompt := promptui.Prompt{ + Label: promptStr, + Validate: validateDuration, + } + + durationStr, err := prompt.Run() + if err != nil { + return 0, err + } + + return time.ParseDuration(durationStr) +} + func (*realPrompter) CaptureFujiDuration(promptStr string) (time.Duration, error) { prompt := promptui.Prompt{ Label: promptStr, @@ -258,7 +291,7 @@ func (*realPrompter) CaptureID(promptStr string) (ids.ID, error) { func (*realPrompter) CaptureNodeID(promptStr string) (ids.NodeID, error) { prompt := promptui.Prompt{ Label: promptStr, - Validate: validateNodeID, + Validate: ValidateNodeID, } nodeIDStr, err := prompt.Run() @@ -268,6 +301,22 @@ func (*realPrompter) CaptureNodeID(promptStr string) (ids.NodeID, error) { return ids.NodeIDFromString(nodeIDStr) } +func (*realPrompter) CaptureValidatorBalance( + promptStr string, + availableBalance uint64, +) (uint64, error) { + prompt := promptui.Prompt{ + Label: promptStr, + Validate: validateValidatorBalanceFunc(availableBalance), + } + amountStr, err := prompt.Run() + if err != nil { + return 0, err + } + + return strconv.ParseUint(amountStr, 10, 64) +} + func (*realPrompter) CaptureWeight(promptStr string) (uint64, error) { prompt := promptui.Prompt{ Label: promptStr, @@ -282,15 +331,15 @@ func (*realPrompter) CaptureWeight(promptStr string) (uint64, error) { return strconv.ParseUint(amountStr, 10, 64) } -func (*realPrompter) CaptureInt(promptStr string) (int, error) { +func (*realPrompter) CaptureInt(promptStr string, validator func(int) error) (int, error) { prompt := promptui.Prompt{ Label: promptStr, Validate: func(input string) error { - _, err := strconv.Atoi(input) + val, err := strconv.Atoi(input) if err != nil { return err } - return nil + return validator(val) }, } input, err := prompt.Run() @@ -304,6 +353,50 @@ func (*realPrompter) CaptureInt(promptStr string) (int, error) { return val, nil } +func (*realPrompter) CaptureUint8(promptStr string) (uint8, error) { + prompt := promptui.Prompt{ + Label: promptStr, + Validate: func(input string) error { + _, err := strconv.ParseUint(input, 0, 8) + if err != nil { + return err + } + return nil + }, + } + input, err := prompt.Run() + if err != nil { + return 0, err + } + val, err := strconv.ParseUint(input, 0, 8) + if err != nil { + return 0, err + } + return uint8(val), nil +} + +func (*realPrompter) CaptureUint16(promptStr string) (uint16, error) { + prompt := promptui.Prompt{ + Label: promptStr, + Validate: func(input string) error { + _, err := strconv.ParseUint(input, 0, 16) + if err != nil { + return err + } + return nil + }, + } + input, err := prompt.Run() + if err != nil { + return 0, err + } + val, err := strconv.ParseUint(input, 0, 16) + if err != nil { + return 0, err + } + return uint16(val), nil +} + func (*realPrompter) CaptureUint32(promptStr string) (uint32, error) { prompt := promptui.Prompt{ Label: promptStr, diff --git a/pkg/prompts/validations.go b/pkg/prompts/validations.go index bfe2b54a6..8b13cdddf 100644 --- a/pkg/prompts/validations.go +++ b/pkg/prompts/validations.go @@ -72,6 +72,22 @@ func validateFujiStakingDuration(input string) error { return nil } +func validateEtnaDuration(input string) error { + d, err := time.ParseDuration(input) + if err != nil { + return err + } + if d < constants.StakingEtnaMinimumDuration { + return fmt.Errorf("below the minimum staking duration of %s", ux.FormatDuration(constants.StakingEtnaMinimumDuration)) + } + return nil +} + +func validateDuration(input string) error { + _, err := time.ParseDuration(input) + return err +} + func validateTime(input string) error { t, err := time.Parse(constants.TimeParseLayout, input) if err != nil { @@ -83,7 +99,7 @@ func validateTime(input string) error { return err } -func validateNodeID(input string) error { +func ValidateNodeID(input string) error { _, err := ids.NodeIDFromString(input) return err } @@ -129,6 +145,22 @@ func validateWeight(input string) error { return nil } +func validateValidatorBalanceFunc(availableBalance uint64) func(string) error { + return func(input string) error { + val, err := strconv.ParseUint(input, 10, 64) + if err != nil { + return err + } + if val < 1 { + return fmt.Errorf("subnet validator balance must be at least 1 AVAX") + } + if val > availableBalance { + return fmt.Errorf("balance should be less than %d AVAX", availableBalance) + } + return nil + } +} + func validateBiggerThanZero(input string) error { val, err := strconv.ParseUint(input, 0, 64) if err != nil { @@ -354,3 +386,10 @@ func ValidateHexa(input string) error { } return err } + +func ValidatePositiveInt(val int) error { + if val <= 0 { + return fmt.Errorf("value must be greater than cero") + } + return nil +} diff --git a/pkg/remoteconfig/avalanche.go b/pkg/remoteconfig/avalanche.go index 835e99851..777539fe9 100644 --- a/pkg/remoteconfig/avalanche.go +++ b/pkg/remoteconfig/avalanche.go @@ -13,35 +13,39 @@ import ( ) type AvalancheConfigInputs struct { - HTTPHost string - APIAdminEnabled bool - IndexEnabled bool - NetworkID string - DBDir string - LogDir string - PublicIP string - StateSyncEnabled bool - PruningEnabled bool - Aliases []string - BlockChainID string - TrackSubnets string - BootstrapIDs string - BootstrapIPs string - GenesisPath string + HTTPHost string + APIAdminEnabled bool + IndexEnabled bool + NetworkID string + DBDir string + LogDir string + PublicIP string + StateSyncEnabled bool + PruningEnabled bool + Aliases []string + BlockChainID string + TrackSubnets string + BootstrapIDs string + BootstrapIPs string + PartialSync bool + GenesisPath string + UpgradePath string + ProposerVMUseCurrentHeight bool } func PrepareAvalancheConfig(publicIP string, networkID string, subnets []string) AvalancheConfigInputs { return AvalancheConfigInputs{ - HTTPHost: "127.0.0.1", - NetworkID: networkID, - DBDir: "/.avalanchego/db/", - LogDir: "/.avalanchego/logs/", - PublicIP: publicIP, - StateSyncEnabled: true, - PruningEnabled: false, - TrackSubnets: strings.Join(subnets, ","), - Aliases: nil, - BlockChainID: "", + HTTPHost: "127.0.0.1", + NetworkID: networkID, + DBDir: "/.avalanchego/db/", + LogDir: "/.avalanchego/logs/", + PublicIP: publicIP, + StateSyncEnabled: true, + PruningEnabled: false, + TrackSubnets: strings.Join(subnets, ","), + Aliases: nil, + BlockChainID: "", + ProposerVMUseCurrentHeight: constants.DevnetFlagsProposerVMUseCurrentHeight, } } @@ -102,6 +106,10 @@ func GetRemoteAvalancheGenesis() string { return filepath.Join(constants.CloudNodeConfigPath, constants.GenesisFileName) } +func GetRemoteAvalancheUpgrade() string { + return filepath.Join(constants.CloudNodeConfigPath, constants.UpgradeFileName) +} + func GetRemoteAvalancheAliasesConfig() string { return filepath.Join(constants.CloudNodeConfigPath, "chains", constants.AliasesFileName) } diff --git a/pkg/remoteconfig/templates/avalanche-node.tmpl b/pkg/remoteconfig/templates/avalanche-node.tmpl index c5704fd1f..dfd9e318e 100644 --- a/pkg/remoteconfig/templates/avalanche-node.tmpl +++ b/pkg/remoteconfig/templates/avalanche-node.tmpl @@ -2,7 +2,9 @@ "http-host": "{{.HTTPHost}}", "api-admin-enabled": {{.APIAdminEnabled}}, "index-enabled": {{.IndexEnabled}}, + "proposervm-use-current-height-bool": {{.ProposerVMUseCurrentHeight}}, "network-id": "{{if .NetworkID}}{{.NetworkID}}{{else}}fuji{{end}}", + "partial-sync-primary-network": "{{ .PartialSync }}", {{- if .BootstrapIDs }} "bootstrap-ids": "{{ .BootstrapIDs }}", {{- end }} @@ -12,6 +14,9 @@ {{- if .GenesisPath }} "genesis-file": "{{ .GenesisPath }}", {{- end }} +{{- if .UpgradePath }} + "upgrade-file": "{{ .UpgradePath }}", +{{- end }} {{- if .PublicIP }} "public-ip": "{{.PublicIP}}", {{- else }} diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 13b867db3..858c6c72e 100644 --- a/pkg/ssh/ssh.go +++ b/pkg/ssh/ssh.go @@ -24,6 +24,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/remoteconfig" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanche-cli/pkg/constants" @@ -177,13 +178,23 @@ func RunSSHStopAWMRelayerService(host *models.Host) error { } // RunSSHUpgradeAvalanchego runs script to upgrade avalanchego -func RunSSHUpgradeAvalanchego(host *models.Host, network models.Network, avalancheGoVersion string, publicAccessToHTTPPort bool) error { +func RunSSHUpgradeAvalanchego(host *models.Host, avalancheGoVersion string) error { withMonitoring, err := docker.WasNodeSetupWithMonitoring(host) if err != nil { return err } - - if err := docker.ComposeSSHSetupNode(host, network, avalancheGoVersion, withMonitoring, publicAccessToHTTPPort); err != nil { + if err := docker.ComposeOverSSH("Compose Node", + host, + constants.SSHScriptTimeout, + "templates/avalanchego.docker-compose.yml", + docker.DockerComposeInputs{ + AvalanchegoVersion: avalancheGoVersion, + WithMonitoring: withMonitoring, + WithAvalanchego: true, + E2E: utils.IsE2E(), + E2EIP: utils.E2EConvertIP(host.IP), + E2ESuffix: utils.E2ESuffix(host.IP), + }); err != nil { return err } return docker.RestartDockerCompose(host, constants.SSHLongRunningScriptTimeout) @@ -424,14 +435,21 @@ func RunSSHSetupDevNet(host *models.Host, nodeInstanceDirPath string) error { } if err := host.Upload( filepath.Join(nodeInstanceDirPath, constants.GenesisFileName), - filepath.Join(constants.CloudNodeConfigPath, constants.GenesisFileName), + remoteconfig.GetRemoteAvalancheGenesis(), + constants.SSHFileOpsTimeout, + ); err != nil { + return err + } + if err := host.Upload( + filepath.Join(nodeInstanceDirPath, constants.UpgradeFileName), + remoteconfig.GetRemoteAvalancheUpgrade(), constants.SSHFileOpsTimeout, ); err != nil { return err } if err := host.Upload( filepath.Join(nodeInstanceDirPath, constants.NodeFileName), - filepath.Join(constants.CloudNodeConfigPath, constants.NodeFileName), + remoteconfig.GetRemoteAvalancheNodeConfig(), constants.SSHFileOpsTimeout, ); err != nil { return err @@ -555,7 +573,10 @@ func RunSSHRenderAvalancheNodeConfig( if genesisFileExists(host) { avagoConf.GenesisPath = filepath.Join(constants.DockerNodeConfigPath, constants.GenesisFileName) } - if network.Kind == models.Local || network.Kind == models.Devnet || isAPIHost { + if upgradeFileExists(host) { + avagoConf.UpgradePath = filepath.Join(constants.DockerNodeConfigPath, constants.UpgradeFileName) + } + if network.Kind == models.Local || network.Kind == models.Devnet || network.Kind == models.EtnaDevnet || isAPIHost { avagoConf.HTTPHost = "0.0.0.0" } remoteAvagoConf, err := getAvalancheGoConfigData(host) @@ -567,6 +588,13 @@ func RunSSHRenderAvalancheNodeConfig( bootstrapIPs, _ := utils.StringValue(remoteAvagoConf, "bootstrap-ips") avagoConf.BootstrapIDs = bootstrapIDs avagoConf.BootstrapIPs = bootstrapIPs + partialSyncI, ok := remoteAvagoConf[config.PartialSyncPrimaryNetworkKey] + if ok { + partialSync, ok := partialSyncI.(bool) + if ok { + avagoConf.PartialSync = partialSync + } + } } // ready to render node config nodeConf, err := remoteconfig.RenderAvalancheNodeConfig(avagoConf) @@ -898,6 +926,11 @@ func genesisFileExists(host *models.Host) bool { return genesisFileExists } +func upgradeFileExists(host *models.Host) bool { + upgradeFileExists, _ := host.FileExists(filepath.Join(constants.CloudNodeConfigPath, constants.UpgradeFileName)) + return upgradeFileExists +} + func nodeConfigFileExists(host *models.Host) bool { nodeConfigFileExists, _ := host.FileExists(remoteconfig.GetRemoteAvalancheNodeConfig()) return nodeConfigFileExists diff --git a/pkg/subnet/helpers.go b/pkg/subnet/helpers.go index 49b1e0b80..332602b68 100644 --- a/pkg/subnet/helpers.go +++ b/pkg/subnet/helpers.go @@ -3,12 +3,23 @@ package subnet import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "unicode" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/key" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" ) +var errIllegalNameCharacter = errors.New( + "illegal name character: only letters, no special characters allowed") + func GetDefaultSubnetAirdropKeyInfo(app *application.Avalanche, subnetName string) (string, string, string, error) { keyName := utils.GetDefaultBlockchainAirdropKeyName(subnetName) keyPath := app.GetKeyPath(keyName) @@ -67,3 +78,66 @@ func GetSubnetAirdropKeyInfo( } return "", "", "", nil } + +func ValidateSubnetNameAndGetChains(app *application.Avalanche, args []string) ([]string, error) { + // this should not be necessary but some bright guy might just be creating + // the genesis by hand or something... + if err := checkInvalidSubnetNames(args[0]); err != nil { + return nil, fmt.Errorf("subnet name %s is invalid: %w", args[0], err) + } + // Check subnet exists + // TODO create a file that lists chains by subnet for fast querying + chains, err := getChainsInSubnet(app, args[0]) + if err != nil { + return nil, fmt.Errorf("failed to getChainsInSubnet: %w", err) + } + + if len(chains) == 0 { + return nil, errors.New("Invalid subnet " + args[0]) + } + + return chains, nil +} + +func checkInvalidSubnetNames(name string) error { + // this is currently exactly the same code as in avalanchego/vms/platformvm/create_chain_tx.go + for _, r := range name { + if r > unicode.MaxASCII || !(unicode.IsLetter(r) || unicode.IsNumber(r) || r == ' ') { + return errIllegalNameCharacter + } + } + return nil +} + +func getChainsInSubnet(app *application.Avalanche, blockchainName string) ([]string, error) { + subnets, err := os.ReadDir(app.GetSubnetDir()) + if err != nil { + return nil, fmt.Errorf("failed to read baseDir: %w", err) + } + + chains := []string{} + + for _, s := range subnets { + if !s.IsDir() { + continue + } + sidecarFile := filepath.Join(app.GetSubnetDir(), s.Name(), constants.SidecarFileName) + if _, err := os.Stat(sidecarFile); err == nil { + // read in sidecar file + jsonBytes, err := os.ReadFile(sidecarFile) + if err != nil { + return nil, fmt.Errorf("failed reading file %s: %w", sidecarFile, err) + } + + var sc models.Sidecar + err = json.Unmarshal(jsonBytes, &sc) + if err != nil { + return nil, fmt.Errorf("failed unmarshaling file %s: %w", sidecarFile, err) + } + if sc.Subnet == blockchainName { + chains = append(chains, sc.Name) + } + } + } + return chains, nil +} diff --git a/pkg/subnet/local.go b/pkg/subnet/local.go index 0d182e965..fc7a5d3d4 100644 --- a/pkg/subnet/local.go +++ b/pkg/subnet/local.go @@ -24,6 +24,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/localnet" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/teleporter" + icmgenesis "github.com/ava-labs/avalanche-cli/pkg/teleporter/genesis" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-network-runner/client" @@ -84,11 +85,12 @@ type setDefaultSnapshotFunc func(string, bool, bool, string, bool) (bool, error) type ICMSpec struct { SkipICMDeploy bool SkipRelayerDeploy bool - Version string + ICMVersion string MessengerContractAddressPath string MessengerDeployerAddressPath string MessengerDeployerTxPath string RegistryBydecodePath string + RelayerVersion string } type DeployInfo struct { @@ -102,25 +104,45 @@ type DeployInfo struct { // * it checks the gRPC is running, if not, it starts it // * kicks off the actual deployment func (d *LocalDeployer) DeployToLocalNetwork( - chain string, + blockchainName string, genesisPath string, icmSpec ICMSpec, subnetIDStr string, + prefix string, ) (*DeployInfo, error) { - if err := d.StartServer(); err != nil { + if err := d.StartServer( + constants.ServerRunFileLocalNetworkPrefix, + binutils.LocalNetworkGRPCServerPort, + binutils.LocalNetworkGRPCGatewayPort, + d.app.GetSnapshotsDir(), + "", + ); err != nil { return nil, err } - return d.doDeploy(chain, genesisPath, icmSpec, subnetIDStr) + return d.doDeploy(blockchainName, genesisPath, icmSpec, subnetIDStr, prefix) } -func (d *LocalDeployer) StartServer() error { - isRunning, err := d.procChecker.IsServerProcessRunning(d.app) +func (d *LocalDeployer) StartServer( + prefix string, + serverPort string, + gatewayPort string, + snapshotsDir string, + logPath string, +) error { + isRunning, err := d.procChecker.IsServerProcessRunning(d.app, prefix) if err != nil { return fmt.Errorf("failed querying if server process is running: %w", err) } if !isRunning { d.app.Log.Debug("gRPC server is not running") - if err := binutils.StartServerProcess(d.app); err != nil { + if err := binutils.StartServerProcess( + d.app, + prefix, + serverPort, + gatewayPort, + snapshotsDir, + logPath, + ); err != nil { return fmt.Errorf("failed starting gRPC server process: %w", err) } d.backendStartedHere = true @@ -154,13 +176,19 @@ func (d *LocalDeployer) BackendStartedHere() bool { // - deploy a new blockchain for the given VM ID, genesis, and available subnet ID // - waits completion of operation // - show status -func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSpec, subnetIDStr string) (*DeployInfo, error) { +func (d *LocalDeployer) doDeploy( + blockchainName string, + genesisPath string, + icmSpec ICMSpec, + subnetIDStr string, + prefix string, +) (*DeployInfo, error) { needsRestart, avalancheGoBinPath, err := d.SetupLocalEnv() if err != nil { return nil, err } - backendLogFile, err := binutils.GetBackendLogFile(d.app) + backendLogFile, err := binutils.GetBackendLogFile(d.app, prefix) var backendLogDir string if err == nil { // TODO should we do something if there _was_ an error? @@ -177,7 +205,7 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp defer cancel() // loading sidecar before it's needed so we catch any error early - sc, err := d.app.LoadSidecar(chain) + sc, err := d.app.LoadSidecar(blockchainName) if err != nil { return nil, fmt.Errorf("failed to load sidecar: %w", err) } @@ -195,9 +223,9 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp } } - chainVMID, err := anrutils.VMID(chain) + chainVMID, err := anrutils.VMID(blockchainName) if err != nil { - return nil, fmt.Errorf("failed to create VM ID from %s: %w", chain, err) + return nil, fmt.Errorf("failed to create VM ID from %s: %w", blockchainName, err) } d.app.Log.Debug("this VM will get ID", zap.String("vm-id", chainVMID.String())) @@ -256,7 +284,7 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp logRootDir = clusterInfo.GetLogRootDir() if alreadyDeployed(chainVMID, clusterInfo) { - return nil, fmt.Errorf("subnet %s has already been deployed", chain) + return nil, fmt.Errorf("subnet %s has already been deployed", blockchainName) } numBlockchains := len(clusterInfo.CustomChains) @@ -281,11 +309,11 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp // if a chainConfig has been configured var ( chainConfig string - chainConfigFile = filepath.Join(d.app.GetSubnetDir(), chain, constants.ChainConfigFileName) + chainConfigFile = filepath.Join(d.app.GetSubnetDir(), blockchainName, constants.ChainConfigFileName) perNodeChainConfig string - perNodeChainConfigFile = filepath.Join(d.app.GetSubnetDir(), chain, constants.PerNodeChainConfigFileName) + perNodeChainConfigFile = filepath.Join(d.app.GetSubnetDir(), blockchainName, constants.PerNodeChainConfigFileName) subnetConfig string - subnetConfigFile = filepath.Join(d.app.GetSubnetDir(), chain, constants.SubnetConfigFileName) + subnetConfigFile = filepath.Join(d.app.GetSubnetDir(), blockchainName, constants.SubnetConfigFileName) ) if _, err := os.Stat(chainConfigFile); err == nil { // currently the ANR only accepts the file as a path, not its content @@ -310,14 +338,14 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp // the given VM ID, genesis, and available subnet ID blockchainSpecs := []*rpcpb.BlockchainSpec{ { - VmName: chain, + VmName: blockchainName, Genesis: genesisPath, SubnetId: &subnetIDStr, SubnetSpec: &rpcpb.SubnetSpec{ SubnetConfig: subnetConfig, }, ChainConfig: chainConfig, - BlockchainAlias: chain, + BlockchainAlias: blockchainName, PerNodeChainConfig: perNodeChainConfig, }, } @@ -387,8 +415,8 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp } else { icmVersion := "" switch { - case icmSpec.Version != "" && icmSpec.Version != "latest": - icmVersion = icmSpec.Version + case icmSpec.ICMVersion != "" && icmSpec.ICMVersion != "latest": + icmVersion = icmSpec.ICMVersion case sc.TeleporterVersion != "": icmVersion = sc.TeleporterVersion default: @@ -415,6 +443,7 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp cChainKey.PrivKeyHex(), true, true, + false, ) if err != nil { return nil, err @@ -426,35 +455,65 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp } // deploy current blockchain ux.Logger.PrintToUser("") - subnetID, blockchainID, err := utils.GetChainIDs(network.Endpoint, chain) + subnetID, blockchainID, err := utils.GetChainIDs(network.Endpoint, blockchainName) if err != nil { return nil, err } - teleporterKeyName := sc.TeleporterKey - if teleporterKeyName == "" { - genesisData, err := d.app.LoadRawGenesis(chain) + blockchainKeyName := sc.TeleporterKey + if blockchainKeyName == "" { + genesisData, err := d.app.LoadRawGenesis(blockchainName) if err != nil { return nil, err } - teleporterKeyName, _, _, err = GetSubnetAirdropKeyInfo(d.app, network, chain, genesisData) + blockchainKeyName, _, _, err = GetSubnetAirdropKeyInfo(d.app, network, blockchainName, genesisData) if err != nil { return nil, err } } - blockchainKey, err := key.LoadSoft(network.ID, d.app.GetKeyPath(teleporterKeyName)) + blockchainKey, err := key.LoadSoft(network.ID, d.app.GetKeyPath(blockchainKeyName)) if err != nil { return nil, err } - _, icmMessengerAddress, icmRegistryAddress, err = icmd.Deploy( - chain, - network.BlockchainEndpoint(blockchainID), - blockchainKey.PrivKeyHex(), - true, - true, - ) + genesisData, err := os.ReadFile(genesisPath) + if err != nil { + return nil, err + } + messengerAtGenesis, registryAtGenesis, err := icmgenesis.ICMAtGenesis(genesisData) if err != nil { return nil, err } + switch { + case registryAtGenesis: + ux.Logger.PrintToUser("Teleporter Messenger and Registry already included in %s's Genesis", blockchainName) + icmMessengerAddress = icmgenesis.MessengerContractAddress + icmRegistryAddress = icmgenesis.RegistryContractAddress + case messengerAtGenesis: + ux.Logger.PrintToUser("Teleporter Messenger already included in %s's Genesis", blockchainName) + icmMessengerAddress = icmgenesis.MessengerContractAddress + _, _, icmRegistryAddress, err = icmd.Deploy( + blockchainName, + network.BlockchainEndpoint(blockchainID), + blockchainKey.PrivKeyHex(), + false, + true, + false, + ) + if err != nil { + return nil, err + } + default: + _, icmMessengerAddress, icmRegistryAddress, err = icmd.Deploy( + blockchainName, + network.BlockchainEndpoint(blockchainID), + blockchainKey.PrivKeyHex(), + true, + true, + false, + ) + if err != nil { + return nil, err + } + } if sc.RunRelayer && !icmSpec.SkipRelayerDeploy { if !cchainAlreadyDeployed { if err := teleporter.FundRelayer( @@ -505,7 +564,7 @@ func (d *LocalDeployer) doDeploy(chain string, genesisPath string, icmSpec ICMSp ux.Logger.PrintToUser("") // start relayer if err := teleporter.DeployRelayer( - "latest", + icmSpec.RelayerVersion, d.app.GetAWMRelayerBinDir(), relayerConfigPath, d.app.GetLocalRelayerLogPath(models.Local), diff --git a/pkg/subnet/local_test.go b/pkg/subnet/local_test.go index 26dd939ef..b29d1e313 100644 --- a/pkg/subnet/local_test.go +++ b/pkg/subnet/local_test.go @@ -80,7 +80,7 @@ func TestDeployToLocal(t *testing.T) { // fake-return true simulating the process is running procChecker := &mocks.ProcessChecker{} - procChecker.On("IsServerProcessRunning", mock.Anything).Return(true, nil) + procChecker.On("IsServerProcessRunning", mock.Anything, mock.Anything).Return(true, nil) tmpDir := os.TempDir() testDir, err := os.MkdirTemp(tmpDir, "local-test") @@ -143,7 +143,7 @@ func TestDeployToLocal(t *testing.T) { icmSpec := ICMSpec{ SkipICMDeploy: true, } - deployInfo, err := testDeployer.DeployToLocalNetwork(testChainName, testGenesis.Name(), icmSpec, "") + deployInfo, err := testDeployer.DeployToLocalNetwork(testChainName, testGenesis.Name(), icmSpec, "", "") require.NoError(err) require.Equal(testSubnetID2, deployInfo.SubnetID.String()) require.Equal(testBlockChainID2, deployInfo.BlockchainID.String()) diff --git a/pkg/subnet/public.go b/pkg/subnet/public.go index f44529429..db25402fb 100644 --- a/pkg/subnet/public.go +++ b/pkg/subnet/public.go @@ -8,8 +8,13 @@ import ( "fmt" "time" + avagofee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + goethereumcommon "github.com/ethereum/go-ethereum/common" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -33,6 +38,8 @@ import ( "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) +const showFees = true + var ErrNoSubnetAuthKeysInWallet = errors.New("auth wallet does not contain subnet auth keys") type PublicDeployer struct { @@ -58,7 +65,7 @@ func NewPublicDeployer(app *application.Avalanche, kc *keychain.Keychain, networ // - signs the tx with the wallet as the owner of fee outputs and a possible subnet auth key // - if partially signed, returns the tx so that it can later on be signed by the rest of the subnet auth keys // - if fully signed, issues it -func (d *PublicDeployer) AddValidator( +func (d *PublicDeployer) AddValidatorNonSOV( waitForTxAcceptance bool, controlKeys []string, subnetAuthKeysStrs []string, @@ -111,6 +118,94 @@ func (d *PublicDeployer) AddValidator( return false, tx, remainingSubnetAuthKeys, nil } +func (d *PublicDeployer) SetL1ValidatorWeight( + message *warp.Message, +) (ids.ID, *txs.Tx, error) { + wallet, err := d.loadCacheWallet() + if err != nil { + return ids.Empty, nil, err + } + tx, err := d.createSetSubnetValidatorWeightTx( + message, + wallet, + ) + if err != nil { + return ids.Empty, nil, err + } + id, err := d.Commit(tx, true) + return id, tx, err +} + +func (*PublicDeployer) createSetSubnetValidatorWeightTx( + message *warp.Message, + wallet primary.Wallet, +) (*txs.Tx, error) { + unsignedTx, err := wallet.P().Builder().NewSetL1ValidatorWeightTx( + message.Bytes(), + ) + if err != nil { + return nil, fmt.Errorf("error building tx: %w", err) + } + if unsignedTx != nil { + if err := printFee("SetSubnetValidatorWeightTX", wallet, unsignedTx); err != nil { + return nil, err + } + } + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return nil, fmt.Errorf("error signing tx: %w", err) + } + return &tx, nil +} + +func (d *PublicDeployer) RegisterL1Validator( + balance uint64, + pop signer.ProofOfPossession, + message *warp.Message, +) (ids.ID, *txs.Tx, error) { + wallet, err := d.loadCacheWallet() + if err != nil { + return ids.Empty, nil, err + } + tx, err := d.createRegisterSubnetValidatorTx( + balance, + pop, + message, + wallet, + ) + if err != nil { + return ids.Empty, nil, err + } + id, err := d.Commit(tx, true) + return id, tx, err +} + +func (*PublicDeployer) createRegisterSubnetValidatorTx( + balance uint64, + pop signer.ProofOfPossession, + message *warp.Message, + wallet primary.Wallet, +) (*txs.Tx, error) { + unsignedTx, err := wallet.P().Builder().NewRegisterL1ValidatorTx( + balance, + pop.ProofOfPossession, + message.Bytes(), + ) + if err != nil { + return nil, fmt.Errorf("error building tx: %w", err) + } + if unsignedTx != nil { + if err := printFee("RegisterSubnetValidatorTX", wallet, unsignedTx); err != nil { + return nil, err + } + } + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return nil, fmt.Errorf("error signing tx: %w", err) + } + return &tx, nil +} + // change subnet owner for [subnetID] // - creates a transfer subnet ownership tx // - sets the change output owner to be a wallet address (if not, it may go to any other subnet auth address) @@ -360,6 +455,87 @@ func (d *PublicDeployer) DeployBlockchain( return isFullySigned, id, tx, remainingSubnetAuthKeys, nil } +func (d *PublicDeployer) ConvertL1( + controlKeys []string, + subnetAuthKeysStrs []string, + subnetID ids.ID, + chainID ids.ID, + validatorManagerAddress goethereumcommon.Address, + validators []*txs.ConvertSubnetToL1Validator, +) (bool, ids.ID, *txs.Tx, []string, error) { + ux.Logger.PrintToUser("Now calling ConvertL1 Tx...") + + wallet, err := d.loadCacheWallet(subnetID) + if err != nil { + return false, ids.Empty, nil, nil, err + } + + subnetAuthKeys, err := address.ParseToIDs(subnetAuthKeysStrs) + if err != nil { + return false, ids.Empty, nil, nil, fmt.Errorf("failure parsing subnet auth keys: %w", err) + } + + showLedgerSignatureMsg(d.kc.UsesLedger, d.kc.HasOnlyOneKey(), "ConvertL1 transaction") + + tx, err := d.createConvertL1Tx(subnetAuthKeys, subnetID, chainID, validatorManagerAddress.Bytes(), validators, wallet) + if err != nil { + return false, ids.Empty, nil, nil, err + } + + _, remainingSubnetAuthKeys, err := txutils.GetRemainingSigners(tx, controlKeys) + if err != nil { + return false, ids.Empty, nil, nil, err + } + isFullySigned := len(remainingSubnetAuthKeys) == 0 + + id := ids.Empty + if isFullySigned { + id, err = d.Commit(tx, true) + if err != nil { + return false, ids.Empty, nil, nil, err + } + } + + return isFullySigned, id, tx, remainingSubnetAuthKeys, nil +} + +func (d *PublicDeployer) PChainTransfer( + destination ids.ShortID, + amount uint64, +) (ids.ID, *txs.Tx, error) { + wallet, err := d.loadCacheWallet() + if err != nil { + return ids.Empty, nil, err + } + to := secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{destination}, + } + output := &avax.TransferableOutput{ + Asset: avax.Asset{ID: wallet.P().Builder().Context().AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: to, + }, + } + outputs := []*avax.TransferableOutput{output} + unsignedTx, err := wallet.P().Builder().NewBaseTx( + outputs, + ) + if err != nil { + return ids.Empty, nil, err + } + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return ids.Empty, nil, err + } + id, err := d.Commit(&tx, true) + if err != nil { + return ids.Empty, nil, err + } + return id, &tx, nil +} + func (d *PublicDeployer) Commit( tx *txs.Tx, waitForTxAcceptance bool, @@ -393,7 +569,7 @@ func (d *PublicDeployer) Commit( time.Sleep(sleepBetweenRepeats) } if issueTxErr != nil { - d.cleanCacheWallet() + d.CleanCacheWallet() } return tx.ID(), issueTxErr } @@ -447,7 +623,7 @@ func (d *PublicDeployer) loadWallet(subnetIDs ...ids.ID) (primary.Wallet, error) return wallet, nil } -func (d *PublicDeployer) cleanCacheWallet() { +func (d *PublicDeployer) CleanCacheWallet() { d.wallet = nil } @@ -499,6 +675,11 @@ func (d *PublicDeployer) createBlockchainTx( if err != nil { return nil, fmt.Errorf("error building tx: %w", err) } + if unsignedTx != nil { + if err := printFee("CreateChainTx", wallet, unsignedTx); err != nil { + return nil, err + } + } tx := txs.Tx{Unsigned: unsignedTx} // sign with current wallet if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { @@ -507,6 +688,37 @@ func (d *PublicDeployer) createBlockchainTx( return &tx, nil } +func (d *PublicDeployer) createConvertL1Tx( + subnetAuthKeys []ids.ShortID, + subnetID ids.ID, + chainID ids.ID, + address []byte, + validators []*txs.ConvertSubnetToL1Validator, + wallet primary.Wallet, +) (*txs.Tx, error) { + options := d.getMultisigTxOptions(subnetAuthKeys) + unsignedTx, err := wallet.P().Builder().NewConvertSubnetToL1Tx( + subnetID, + chainID, + address, + validators, + options..., + ) + if err != nil { + return nil, fmt.Errorf("error building tx: %w", err) + } + if unsignedTx != nil { + if err := printFee("ConvertSubnetTX", wallet, unsignedTx); err != nil { + return nil, err + } + } + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return nil, fmt.Errorf("error signing tx: %w", err) + } + return &tx, nil +} + func (d *PublicDeployer) createTransferSubnetOwnershipTx( subnetAuthKeys []ids.ShortID, subnetID ids.ID, @@ -691,6 +903,11 @@ func (d *PublicDeployer) createSubnetTx(controlKeys []string, threshold uint32, unsignedTx, err := wallet.P().Builder().NewCreateSubnetTx( owners, ) + if unsignedTx != nil { + if err := printFee("CreateSubnetTx", wallet, unsignedTx); err != nil { + return ids.Empty, err + } + } if err != nil { return ids.Empty, fmt.Errorf("error building tx: %w", err) } @@ -702,6 +919,30 @@ func (d *PublicDeployer) createSubnetTx(controlKeys []string, threshold uint32, return d.Commit(&tx, true) } +func printFee(kind string, wallet primary.Wallet, unsignedTx txs.UnsignedTx) error { + if showFees { + var pFeeCalculator avagofee.Calculator + pContext := wallet.P().Builder().Context() + calcKind := "dynamic" + if pContext.GasPrice != 0 { + pFeeCalculator = avagofee.NewDynamicCalculator(pContext.ComplexityWeights, pContext.GasPrice) + } else { + pFeeCalculator = avagofee.NewStaticCalculator(pContext.StaticFeeConfig) + calcKind = "static" + } + txFee, err := pFeeCalculator.CalculateFee(unsignedTx) + if err != nil { + if !errors.Is(err, avagofee.ErrUnsupportedTx) { + return err + } + ux.Logger.PrintToUser(logging.Yellow.Wrap("unable to get %s fee: not supported by %s calculator"), kind, calcKind) + } else { + ux.Logger.PrintToUser(logging.Yellow.Wrap("%s fee: %.9f AVAX"), kind, float64(txFee)/float64(units.Avax)) + } + } + return nil +} + func (d *PublicDeployer) getSubnetAuthAddressesInWallet(subnetAuth []ids.ShortID) []ids.ShortID { walletAddrs := d.kc.Addresses().List() subnetAuthInWallet := []ids.ShortID{} diff --git a/pkg/teleporter/genesis/deployed_messenger_bytecode.txt b/pkg/teleporter/genesis/deployed_messenger_bytecode.txt new file mode 100644 index 000000000..aa3ae3c6c --- /dev/null +++ b/pkg/teleporter/genesis/deployed_messenger_bytecode.txt @@ -0,0 +1 @@ +0x608060405234801561001057600080fd5b506004361061014d5760003560e01c8063a8898181116100c3578063df20e8bc1161007c578063df20e8bc1461033b578063e69d606a1461034e578063e6e67bd5146103b6578063ebc3b1ba146103f2578063ecc7042814610415578063fc2d61971461041e57600080fd5b8063a8898181146102b2578063a9a85614146102c5578063b771b3bc146102d8578063c473eef8146102e6578063ccb5f8091461031f578063d127dc9b1461033257600080fd5b8063399b77da11610115578063399b77da1461021957806362448850146102395780638245a1b01461024c578063860a3b061461025f578063892bf4121461027f5780638ac0fd041461029f57600080fd5b80630af5b4ff1461015257806322296c3a1461016d5780632bc8b0bf146101825780632ca40f55146101955780632e27c223146101ee575b600080fd5b61015a610431565b6040519081526020015b60405180910390f35b61018061017b366004612251565b610503565b005b61015a61019036600461226e565b6105f8565b6101e06101a336600461226e565b6005602090815260009182526040918290208054835180850190945260018201546001600160a01b03168452600290910154918301919091529082565b604051610164929190612287565b6102016101fc36600461226e565b610615565b6040516001600160a01b039091168152602001610164565b61015a61022736600461226e565b60009081526005602052604090205490565b61015a6102473660046122ae565b61069e565b61018061025a366004612301565b6106fc565b61015a61026d36600461226e565b60066020526000908152604090205481565b61029261028d366004612335565b6108a7565b6040516101649190612357565b6101806102ad366004612377565b6108da565b61015a6102c03660046123af565b610b19565b61015a6102d3366004612426565b610b5c565b6102016005600160991b0181565b61015a6102f43660046124be565b6001600160a01b03918216600090815260096020908152604080832093909416825291909152205490565b61018061032d3660046124f7565b610e03565b61015a60025481565b61015a61034936600461226e565b61123d565b61039761035c36600461226e565b600090815260056020908152604091829020825180840190935260018101546001600160a01b03168084526002909101549290910182905291565b604080516001600160a01b039093168352602083019190915201610164565b6103dd6103c436600461226e565b6004602052600090815260409020805460019091015482565b60408051928352602083019190915201610164565b61040561040036600461226e565b611286565b6040519015158152602001610164565b61015a60035481565b61018061042c36600461251e565b61129c565b600254600090806104fe576005600160991b016001600160a01b0316634213cf786040518163ffffffff1660e01b8152600401602060405180830381865afa158015610481573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104a59190612564565b9050806104cd5760405162461bcd60e51b81526004016104c49061257d565b60405180910390fd5b600281905560405181907f1eac640109dc937d2a9f42735a05f794b39a5e3759d681951d671aabbce4b10490600090a25b919050565b3360009081526009602090815260408083206001600160a01b0385168452909152902054806105855760405162461bcd60e51b815260206004820152602860248201527f54656c65706f727465724d657373656e6765723a206e6f2072657761726420746044820152676f2072656465656d60c01b60648201526084016104c4565b3360008181526009602090815260408083206001600160a01b03871680855290835281842093909355518481529192917f3294c84e5b0f29d9803655319087207bc94f4db29f7927846944822773780b88910160405180910390a36105f46001600160a01b03831633836114f7565b5050565b600081815260046020526040812061060f9061155f565b92915050565b6000818152600760205260408120546106825760405162461bcd60e51b815260206004820152602960248201527f54656c65706f727465724d657373656e6765723a206d657373616765206e6f74604482015268081c9958d95a5d995960ba1b60648201526084016104c4565b506000908152600860205260409020546001600160a01b031690565b60006001600054146106c25760405162461bcd60e51b81526004016104c4906125c4565b60026000556106f16106d383612804565b833560009081526004602052604090206106ec90611572565b61167c565b600160005592915050565b60016000541461071e5760405162461bcd60e51b81526004016104c4906125c4565b6002600081815590546107379060408401358435610b19565b6000818152600560209081526040918290208251808401845281548152835180850190945260018201546001600160a01b03168452600290910154838301529081019190915280519192509061079f5760405162461bcd60e51b81526004016104c4906128a7565b6000836040516020016107b29190612b42565b60408051601f19818403018152919052825181516020830120919250146107eb5760405162461bcd60e51b81526004016104c490612b55565b8360400135837f2a211ad4a59ab9d003852404f9c57c690704ee755f3c79d2c2812ad32da99df8868560200151604051610826929190612b9e565b60405180910390a360405163ee5b48eb60e01b81526005600160991b019063ee5b48eb90610858908490600401612c23565b6020604051808303816000875af1158015610877573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061089b9190612564565b50506001600055505050565b604080518082019091526000808252602082015260008381526004602052604090206108d390836118bc565b9392505050565b6001600054146108fc5760405162461bcd60e51b81526004016104c4906125c4565b600260005560018054146109225760405162461bcd60e51b81526004016104c490612c36565b60026001558061098c5760405162461bcd60e51b815260206004820152602f60248201527f54656c65706f727465724d657373656e6765723a207a65726f2061646469746960448201526e1bdb985b0819995948185b5bdd5b9d608a1b60648201526084016104c4565b6001600160a01b0382166109b25760405162461bcd60e51b81526004016104c490612c7b565b6000838152600560205260409020546109dd5760405162461bcd60e51b81526004016104c4906128a7565b6000838152600560205260409020600101546001600160a01b03838116911614610a6f5760405162461bcd60e51b815260206004820152603760248201527f54656c65706f727465724d657373656e6765723a20696e76616c69642066656560448201527f20617373657420636f6e7472616374206164647265737300000000000000000060648201526084016104c4565b6000610a7b8383611981565b600085815260056020526040812060020180549293508392909190610aa1908490612ce5565b909155505060008481526005602052604090819020905185917fc1bfd1f1208927dfbd414041dcb5256e6c9ad90dd61aec3249facbd34ff7b3e191610b03916001019081546001600160a01b0316815260019190910154602082015260400190565b60405180910390a2505060018080556000555050565b60408051306020820152908101849052606081018390526080810182905260009060a0016040516020818303038152906040528051906020012090509392505050565b6000600160005414610b805760405162461bcd60e51b81526004016104c4906125c4565b60026000818155905490866001600160401b03811115610ba257610ba2612607565b604051908082528060200260200182016040528015610be757816020015b6040805180820190915260008082526020820152815260200190600190039081610bc05790505b5090508660005b81811015610d6c5760008a8a83818110610c0a57610c0a612cf8565b90506020020135905060006007600083815260200190815260200160002054905080600003610c8a5760405162461bcd60e51b815260206004820152602660248201527f54656c65706f727465724d657373656e6765723a2072656365697074206e6f7460448201526508199bdd5b9960d21b60648201526084016104c4565b610c958d8783610b19565b8214610d095760405162461bcd60e51b815260206004820152603a60248201527f54656c65706f727465724d657373656e6765723a206d6573736167652049442060448201527f6e6f742066726f6d20736f7572636520626c6f636b636861696e00000000000060648201526084016104c4565b6000828152600860209081526040918290205482518084019093528383526001600160a01b03169082018190528651909190879086908110610d4d57610d4d612cf8565b602002602001018190525050505080610d6590612d0e565b9050610bee565b506040805160c0810182528b815260006020820152610df0918101610d96368b90038b018b612d27565b8152602001600081526020018888808060200260200160405190810160405280939291908181526020018383602002808284376000920182905250938552505060408051928352602080840190915290920152508361167c565b60016000559a9950505050505050505050565b6001805414610e245760405162461bcd60e51b81526004016104c490612c36565b60026001556040516306f8253560e41b815263ffffffff8316600482015260009081906005600160991b0190636f82535090602401600060405180830381865afa158015610e76573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610e9e9190810190612da3565b9150915080610f015760405162461bcd60e51b815260206004820152602960248201527f54656c65706f727465724d657373656e6765723a20696e76616c69642077617260448201526870206d65737361676560b81b60648201526084016104c4565b60208201516001600160a01b03163014610f785760405162461bcd60e51b815260206004820152603260248201527f54656c65706f727465724d657373656e6765723a20696e76616c6964206f726960448201527167696e2073656e646572206164647265737360701b60648201526084016104c4565b60008260400151806020019051810190610f929190612f40565b90506000610f9e610431565b90508082604001511461100d5760405162461bcd60e51b815260206004820152603160248201527f54656c65706f727465724d657373656e6765723a20696e76616c6964206465736044820152701d1a5b985d1a5bdb8818da185a5b881251607a1b60648201526084016104c4565b8351825160009161101f918490610b19565b600081815260076020526040902054909150156110945760405162461bcd60e51b815260206004820152602d60248201527f54656c65706f727465724d657373656e6765723a206d65737361676520616c7260448201526c1958591e481c9958d95a5d9959609a1b60648201526084016104c4565b6110a2338460a00151611ae9565b6111005760405162461bcd60e51b815260206004820152602960248201527f54656c65706f727465724d657373656e6765723a20756e617574686f72697a6560448201526832103932b630bcb2b960b91b60648201526084016104c4565b61110e818460000151611b61565b6001600160a01b0386161561114557600081815260086020526040902080546001600160a01b0319166001600160a01b0388161790555b60c08301515160005b81811015611192576111828488600001518760c00151848151811061117557611175612cf8565b6020026020010151611bd3565b61118b81612d0e565b905061114e565b50604080518082018252855181526001600160a01b038916602080830191909152885160009081526004909152919091206111cc91611cfb565b336001600160a01b03168660000151837f292ee90bbaf70b5d4936025e09d56ba08f3e421156b6a568cf3c2840d9343e348a8860405161120d929190613150565b60405180910390a460e0840151511561122f5761122f82876000015186611d57565b505060018055505050505050565b600254600090806112605760405162461bcd60e51b81526004016104c49061257d565b600060035460016112719190612ce5565b905061127e828583610b19565b949350505050565b600081815260076020526040812054151561060f565b60018054146112bd5760405162461bcd60e51b81526004016104c490612c36565b60026001819055546000906112d59084908435610b19565b600081815260066020526040902054909150806113045760405162461bcd60e51b81526004016104c4906128a7565b80836040516020016113169190612b42565b60405160208183030381529060405280519060200120146113495760405162461bcd60e51b81526004016104c490612b55565b600061135b6080850160608601612251565b6001600160a01b03163b116113cf5760405162461bcd60e51b815260206004820152603460248201527f54656c65706f727465724d657373656e6765723a2064657374696e6174696f6e604482015273206164647265737320686173206e6f20636f646560601b60648201526084016104c4565b604051849083907f34795cc6b122b9a0ae684946319f1e14a577b4e8f9b3dda9ac94c21a54d3188c90600090a360008281526006602090815260408083208390558691611420918701908701612251565b61142d60e0870187613174565b60405160240161144094939291906131ba565b60408051601f198184030181529190526020810180516001600160e01b031663643477d560e11b179052905060006114886114816080870160608801612251565b5a84611e8a565b9050806114eb5760405162461bcd60e51b815260206004820152602b60248201527f54656c65706f727465724d657373656e6765723a20726574727920657865637560448201526a1d1a5bdb8819985a5b195960aa1b60648201526084016104c4565b50506001805550505050565b6040516001600160a01b03831660248201526044810182905261155a90849063a9059cbb60e01b906064015b60408051601f198184030181529190526020810180516001600160e01b03166001600160e01b031990931692909217909152611ea4565b505050565b8054600182015460009161060f916131e5565b6060600061158960056115848561155f565b611f76565b9050806000036115d85760408051600080825260208201909252906115d0565b60408051808201909152600080825260208201528152602001906001900390816115a95790505b509392505050565b6000816001600160401b038111156115f2576115f2612607565b60405190808252806020026020018201604052801561163757816020015b60408051808201909152600080825260208201528152602001906001900390816116105790505b50905060005b828110156115d05761164e85611f8c565b82828151811061166057611660612cf8565b60200260200101819052508061167590612d0e565b905061163d565b600080611687610431565b9050600060036000815461169a90612d0e565b919050819055905060006116b383876000015184610b19565b90506000604051806101000160405280848152602001336001600160a01b031681526020018860000151815260200188602001516001600160a01b0316815260200188606001518152602001886080015181526020018781526020018860a00151815250905060008160405160200161172c91906131f8565b60405160208183030381529060405290506000808960400151602001511115611794576040890151516001600160a01b031661177a5760405162461bcd60e51b81526004016104c490612c7b565b604089015180516020909101516117919190611981565b90505b6040805180820182528a820151516001600160a01b039081168252602080830185905283518085018552865187830120815280820184815260008a815260058452869020915182555180516001830180546001600160a01b03191691909516179093559101516002909101558a51915190919086907f2a211ad4a59ab9d003852404f9c57c690704ee755f3c79d2c2812ad32da99df890611838908890869061320b565b60405180910390a360405163ee5b48eb60e01b81526005600160991b019063ee5b48eb9061186a908690600401612c23565b6020604051808303816000875af1158015611889573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906118ad9190612564565b50939998505050505050505050565b60408051808201909152600080825260208201526118d98361155f565b82106119315760405162461bcd60e51b815260206004820152602160248201527f5265636569707451756575653a20696e646578206f7574206f6620626f756e646044820152607360f81b60648201526084016104c4565b8260020160008385600001546119479190612ce5565b81526020808201929092526040908101600020815180830190925280548252600101546001600160a01b0316918101919091529392505050565b6040516370a0823160e01b815230600482015260009081906001600160a01b038516906370a0823190602401602060405180830381865afa1580156119ca573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119ee9190612564565b9050611a056001600160a01b038516333086612058565b6040516370a0823160e01b81523060048201526000906001600160a01b038616906370a0823190602401602060405180830381865afa158015611a4c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a709190612564565b9050818111611ad65760405162461bcd60e51b815260206004820152602c60248201527f5361666545524332305472616e7366657246726f6d3a2062616c616e6365206e60448201526b1bdd081a5b98dc99585cd95960a21b60648201526084016104c4565b611ae082826131e5565b95945050505050565b60008151600003611afc5750600161060f565b815160005b81811015611b5657846001600160a01b0316848281518110611b2557611b25612cf8565b60200260200101516001600160a01b031603611b465760019250505061060f565b611b4f81612d0e565b9050611b01565b506000949350505050565b80600003611bc15760405162461bcd60e51b815260206004820152602760248201527f54656c65706f727465724d657373656e6765723a207a65726f206d657373616760448201526665206e6f6e636560c81b60648201526084016104c4565b60009182526007602052604090912055565b6000611be484848460000151610b19565b6000818152600560209081526040918290208251808401845281548152835180850190945260018201546001600160a01b031684526002909101548383015290810191909152805191925090611c3b575050505050565b60008281526005602090815260408083208381556001810180546001600160a01b03191690556002018390558382018051830151878401516001600160a01b0390811686526009855283862092515116855292528220805491929091611ca2908490612ce5565b9250508190555082602001516001600160a01b031684837fd13a7935f29af029349bed0a2097455b91fd06190a30478c575db3f31e00bf578460200151604051611cec919061321e565b60405180910390a45050505050565b6001820180548291600285019160009182611d1583612d0e565b90915550815260208082019290925260400160002082518155910151600190910180546001600160a01b0319166001600160a01b039092169190911790555050565b80608001515a1015611db95760405162461bcd60e51b815260206004820152602560248201527f54656c65706f727465724d657373656e6765723a20696e73756666696369656e604482015264742067617360d81b60648201526084016104c4565b80606001516001600160a01b03163b600003611dda5761155a838383612096565b602081015160e0820151604051600092611df892869260240161323e565b60408051601f198184030181529190526020810180516001600160e01b031663643477d560e11b17905260608301516080840151919250600091611e3d919084611e8a565b905080611e5657611e4f858585612096565b5050505050565b604051849086907f34795cc6b122b9a0ae684946319f1e14a577b4e8f9b3dda9ac94c21a54d3188c90600090a35050505050565b60008060008084516020860160008989f195945050505050565b6000611ef9826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b031661210b9092919063ffffffff16565b80519091501561155a5780806020019051810190611f179190613268565b61155a5760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b60648201526084016104c4565b6000818310611f8557816108d3565b5090919050565b604080518082019091526000808252602082015281546001830154819003611ff65760405162461bcd60e51b815260206004820152601960248201527f5265636569707451756575653a20656d7074792071756575650000000000000060448201526064016104c4565b60008181526002840160208181526040808420815180830190925280548252600180820180546001600160a01b03811685870152888852959094529490556001600160a01b031990921690559061204e908390612ce5565b9093555090919050565b6040516001600160a01b03808516602483015283166044820152606481018290526120909085906323b872dd60e01b90608401611523565b50505050565b806040516020016120a791906131f8565b60408051601f1981840301815282825280516020918201206000878152600690925291902055829084907f4619adc1017b82e02eaefac01a43d50d6d8de4460774bc370c3ff0210d40c985906120fe9085906131f8565b60405180910390a3505050565b606061127e848460008585600080866001600160a01b031685876040516121329190613283565b60006040518083038185875af1925050503d806000811461216f576040519150601f19603f3d011682016040523d82523d6000602084013e612174565b606091505b509150915061218587838387612190565b979650505050505050565b606083156121ff5782516000036121f8576001600160a01b0385163b6121f85760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064016104c4565b508161127e565b61127e83838151156122145781518083602001fd5b8060405162461bcd60e51b81526004016104c49190612c23565b6001600160a01b038116811461224357600080fd5b50565b80356104fe8161222e565b60006020828403121561226357600080fd5b81356108d38161222e565b60006020828403121561228057600080fd5b5035919050565b828152606081016108d3602083018480516001600160a01b03168252602090810151910152565b6000602082840312156122c057600080fd5b81356001600160401b038111156122d657600080fd5b820160e081850312156108d357600080fd5b600061010082840312156122fb57600080fd5b50919050565b60006020828403121561231357600080fd5b81356001600160401b0381111561232957600080fd5b61127e848285016122e8565b6000806040838503121561234857600080fd5b50508035926020909101359150565b815181526020808301516001600160a01b0316908201526040810161060f565b60008060006060848603121561238c57600080fd5b83359250602084013561239e8161222e565b929592945050506040919091013590565b6000806000606084860312156123c457600080fd5b505081359360208301359350604090920135919050565b60008083601f8401126123ed57600080fd5b5081356001600160401b0381111561240457600080fd5b6020830191508360208260051b850101111561241f57600080fd5b9250929050565b60008060008060008086880360a081121561244057600080fd5b8735965060208801356001600160401b038082111561245e57600080fd5b61246a8b838c016123db565b90985096508691506040603f198401121561248457600080fd5b60408a01955060808a013592508083111561249e57600080fd5b50506124ac89828a016123db565b979a9699509497509295939492505050565b600080604083850312156124d157600080fd5b82356124dc8161222e565b915060208301356124ec8161222e565b809150509250929050565b6000806040838503121561250a57600080fd5b823563ffffffff811681146124dc57600080fd5b6000806040838503121561253157600080fd5b8235915060208301356001600160401b0381111561254e57600080fd5b61255a858286016122e8565b9150509250929050565b60006020828403121561257657600080fd5b5051919050565b60208082526027908201527f54656c65706f727465724d657373656e6765723a207a65726f20626c6f636b636040820152661a185a5b88125160ca1b606082015260800190565b60208082526023908201527f5265656e7472616e63794775617264733a2073656e646572207265656e7472616040820152626e637960e81b606082015260800190565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b038111828210171561263f5761263f612607565b60405290565b60405160c081016001600160401b038111828210171561263f5761263f612607565b60405161010081016001600160401b038111828210171561263f5761263f612607565b604051601f8201601f191681016001600160401b03811182821017156126b2576126b2612607565b604052919050565b6000604082840312156126cc57600080fd5b6126d461261d565b905081356126e18161222e565b808252506020820135602082015292915050565b60006001600160401b0382111561270e5761270e612607565b5060051b60200190565b600082601f83011261272957600080fd5b8135602061273e612739836126f5565b61268a565b82815260059290921b8401810191818101908684111561275d57600080fd5b8286015b848110156127815780356127748161222e565b8352918301918301612761565b509695505050505050565b60006001600160401b038211156127a5576127a5612607565b50601f01601f191660200190565b600082601f8301126127c457600080fd5b81356127d26127398261278c565b8181528460208386010111156127e757600080fd5b816020850160208301376000918101602001919091529392505050565b600060e0823603121561281657600080fd5b61281e612645565b8235815261282e60208401612246565b602082015261284036604085016126ba565b60408201526080830135606082015260a08301356001600160401b038082111561286957600080fd5b61287536838701612718565b608084015260c085013591508082111561288e57600080fd5b5061289b368286016127b3565b60a08301525092915050565b60208082526026908201527f54656c65706f727465724d657373656e6765723a206d657373616765206e6f7460408201526508199bdd5b9960d21b606082015260800190565b6000808335601e1984360301811261290457600080fd5b83016020810192503590506001600160401b0381111561292357600080fd5b8060051b360382131561241f57600080fd5b8183526000602080850194508260005b858110156129735781356129588161222e565b6001600160a01b031687529582019590820190600101612945565b509495945050505050565b6000808335601e1984360301811261299557600080fd5b83016020810192503590506001600160401b038111156129b457600080fd5b8060061b360382131561241f57600080fd5b8183526000602080850194508260005b858110156129735781358752828201356129ef8161222e565b6001600160a01b03168784015260409687019691909101906001016129d6565b6000808335601e19843603018112612a2657600080fd5b83016020810192503590506001600160401b03811115612a4557600080fd5b80360382131561241f57600080fd5b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b6000610100823584526020830135612a948161222e565b6001600160a01b0316602085015260408381013590850152612ab860608401612246565b6001600160a01b0316606085015260808381013590850152612add60a08401846128ed565b8260a0870152612af08387018284612935565b92505050612b0160c084018461297e565b85830360c0870152612b148382846129c6565b92505050612b2560e0840184612a0f565b85830360e0870152612b38838284612a54565b9695505050505050565b6020815260006108d36020830184612a7d565b60208082526029908201527f54656c65706f727465724d657373656e6765723a20696e76616c6964206d65736040820152680e6c2ceca40d0c2e6d60bb1b606082015260800190565b606081526000612bb16060830185612a7d565b90506108d3602083018480516001600160a01b03168252602090810151910152565b60005b83811015612bee578181015183820152602001612bd6565b50506000910152565b60008151808452612c0f816020860160208601612bd3565b601f01601f19169290920160200192915050565b6020815260006108d36020830184612bf7565b60208082526025908201527f5265656e7472616e63794775617264733a207265636569766572207265656e7460408201526472616e637960d81b606082015260800190565b60208082526034908201527f54656c65706f727465724d657373656e6765723a207a65726f2066656520617360408201527373657420636f6e7472616374206164647265737360601b606082015260800190565b634e487b7160e01b600052601160045260246000fd5b8082018082111561060f5761060f612ccf565b634e487b7160e01b600052603260045260246000fd5b600060018201612d2057612d20612ccf565b5060010190565b600060408284031215612d3957600080fd5b6108d383836126ba565b80516104fe8161222e565b600082601f830112612d5f57600080fd5b8151612d6d6127398261278c565b818152846020838601011115612d8257600080fd5b61127e826020830160208701612bd3565b805180151581146104fe57600080fd5b60008060408385031215612db657600080fd5b82516001600160401b0380821115612dcd57600080fd5b9084019060608287031215612de157600080fd5b604051606081018181108382111715612dfc57612dfc612607565b604052825181526020830151612e118161222e565b6020820152604083015182811115612e2857600080fd5b612e3488828601612d4e565b6040830152509350612e4b91505060208401612d93565b90509250929050565b600082601f830112612e6557600080fd5b81516020612e75612739836126f5565b82815260059290921b84018101918181019086841115612e9457600080fd5b8286015b84811015612781578051612eab8161222e565b8352918301918301612e98565b600082601f830112612ec957600080fd5b81516020612ed9612739836126f5565b82815260069290921b84018101918181019086841115612ef857600080fd5b8286015b848110156127815760408189031215612f155760008081fd5b612f1d61261d565b8151815284820151612f2e8161222e565b81860152835291830191604001612efc565b600060208284031215612f5257600080fd5b81516001600160401b0380821115612f6957600080fd5b908301906101008286031215612f7e57600080fd5b612f86612667565b82518152612f9660208401612d43565b602082015260408301516040820152612fb160608401612d43565b60608201526080830151608082015260a083015182811115612fd257600080fd5b612fde87828601612e54565b60a08301525060c083015182811115612ff657600080fd5b61300287828601612eb8565b60c08301525060e08301518281111561301a57600080fd5b61302687828601612d4e565b60e08301525095945050505050565b600081518084526020808501945080840160005b838110156129735781516001600160a01b031687529582019590820190600101613049565b600081518084526020808501945080840160005b83811015612973576130a8878351805182526020908101516001600160a01b0316910152565b6040969096019590820190600101613082565b60006101008251845260018060a01b0360208401511660208501526040830151604085015260608301516130fa60608601826001600160a01b03169052565b506080830151608085015260a08301518160a086015261311c82860182613035565b91505060c083015184820360c0860152613136828261306e565b91505060e083015184820360e0860152611ae08282612bf7565b6001600160a01b038316815260406020820181905260009061127e908301846130bb565b6000808335601e1984360301811261318b57600080fd5b8301803591506001600160401b038211156131a557600080fd5b60200191503681900382131561241f57600080fd5b8481526001600160a01b0384166020820152606060408201819052600090612b389083018486612a54565b8181038181111561060f5761060f612ccf565b6020815260006108d360208301846130bb565b606081526000612bb160608301856130bb565b81516001600160a01b03168152602080830151908201526040810161060f565b8381526001600160a01b0383166020820152606060408201819052600090611ae090830184612bf7565b60006020828403121561327a57600080fd5b6108d382612d93565b60008251613295818460208701612bd3565b919091019291505056fea2646970667358221220586881dd1413fe17197100ceb55646481dae802ef65d37df603c3915f51a4b6364736f6c63430008120033 diff --git a/pkg/teleporter/genesis/deployed_registry_bytecode.txt b/pkg/teleporter/genesis/deployed_registry_bytecode.txt new file mode 100644 index 000000000..7b1a0502b --- /dev/null +++ b/pkg/teleporter/genesis/deployed_registry_bytecode.txt @@ -0,0 +1 @@ +0x608060405234801561001057600080fd5b506004361061009e5760003560e01c8063ac473ac311610066578063ac473ac314610124578063b771b3bc1461012d578063c07f47d41461013b578063d127dc9b14610144578063d820e64f1461016b57600080fd5b80630731775d146100a3578063215abce9146100c857806341f34ed9146100db57806346f9ef49146100f05780634c1f08ce14610103575b600080fd5b6100ab600081565b6040516001600160a01b0390911681526020015b60405180910390f35b6100ab6100d63660046107c5565b610173565b6100ee6100e93660046107de565b610184565b005b6100ab6100fe3660046107c5565b6103f9565b610116610111366004610823565b6104be565b6040519081526020016100bf565b6101166101f481565b6100ab6005600160991b0181565b61011660005481565b6101167f000000000000000000000000000000000000000000000000000000000000000081565b6100ab610566565b600061017e826103f9565b92915050565b6040516306f8253560e41b815263ffffffff8216600482015260009081906005600160991b0190636f82535090602401600060405180830381865afa1580156101d1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526101f991908101906108c5565b91509150806102605760405162461bcd60e51b815260206004820152602860248201527f54656c65706f7274657252656769737472793a20696e76616c69642077617270604482015267206d65737361676560c01b60648201526084015b60405180910390fd5b81517f0000000000000000000000000000000000000000000000000000000000000000146102e45760405162461bcd60e51b815260206004820152602b60248201527f54656c65706f7274657252656769737472793a20696e76616c696420736f757260448201526a18d94818da185a5b88125160aa1b6064820152608401610257565b60208201516001600160a01b0316156103595760405162461bcd60e51b815260206004820152603160248201527f54656c65706f7274657252656769737472793a20696e76616c6964206f726967604482015270696e2073656e646572206164647265737360781b6064820152608401610257565b600080836040015180602001905181019061037491906109cd565b90925090506001600160a01b03811630146103e95760405162461bcd60e51b815260206004820152602f60248201527f54656c65706f7274657252656769737472793a20696e76616c6964206465737460448201526e696e6174696f6e206164647265737360881b6064820152608401610257565b6103f282610578565b5050505050565b60008160000361044b5760405162461bcd60e51b815260206004820181905260248201527f54656c65706f7274657252656769737472793a207a65726f2076657273696f6e6044820152606401610257565b6000828152600160205260409020546001600160a01b03168061017e5760405162461bcd60e51b815260206004820152602560248201527f54656c65706f7274657252656769737472793a2076657273696f6e206e6f7420604482015264199bdd5b9960da1b6064820152608401610257565b60006001600160a01b0382166104e65760405162461bcd60e51b815260040161025790610a49565b6001600160a01b0382166000908152600260205260408120549081900361017e5760405162461bcd60e51b815260206004820152602e60248201527f54656c65706f7274657252656769737472793a2070726f746f636f6c2061646460448201526d1c995cdcc81b9bdd08199bdd5b9960921b6064820152608401610257565b60006105736000546103f9565b905090565b80516000036105c95760405162461bcd60e51b815260206004820181905260248201527f54656c65706f7274657252656769737472793a207a65726f2076657273696f6e6044820152606401610257565b80516000908152600160205260409020546001600160a01b0316156106435760405162461bcd60e51b815260206004820152602a60248201527f54656c65706f7274657252656769737472793a2076657273696f6e20616c72656044820152696164792065786973747360b01b6064820152608401610257565b60208101516001600160a01b031661066d5760405162461bcd60e51b815260040161025790610a49565b60005461067c6101f482610a92565b825111156106e35760405162461bcd60e51b815260206004820152602e60248201527f54656c65706f7274657252656769737472793a2076657273696f6e20696e637260448201526d0cadacadce840e8dede40d0d2ced60931b6064820152608401610257565b602082810180518451600090815260018452604080822080546001600160a01b0319166001600160a01b039485161790559251909116815260029092529020548251111561074c5781516020808401516001600160a01b03166000908152600290915260409020555b602082015182516040516001600160a01b03909216917fa5eed93d951a9603d5f7c0a57de79a299dd3dbd5e51429be209d8053a42ab43a90600090a381518110156107c1578151600081815560405183917f30623e953733f6474dabdfbef1103ce15ab73cdc77c6dfad0f9874d167e8a9b091a35b5050565b6000602082840312156107d757600080fd5b5035919050565b6000602082840312156107f057600080fd5b813563ffffffff8116811461080457600080fd5b9392505050565b6001600160a01b038116811461082057600080fd5b50565b60006020828403121561083557600080fd5b81356108048161080b565b634e487b7160e01b600052604160045260246000fd5b6040516060810167ffffffffffffffff8111828210171561087957610879610840565b60405290565b604051601f8201601f1916810167ffffffffffffffff811182821017156108a8576108a8610840565b604052919050565b805180151581146108c057600080fd5b919050565b600080604083850312156108d857600080fd5b825167ffffffffffffffff808211156108f057600080fd5b908401906060828703121561090457600080fd5b61090c610856565b8251815260208084015161091f8161080b565b8282015260408401518381111561093557600080fd5b80850194505087601f85011261094a57600080fd5b83518381111561095c5761095c610840565b61096e601f8201601f1916830161087f565b9350808452888282870101111561098457600080fd5b60005b818110156109a2578581018301518582018401528201610987565b506000828286010152508260408301528195506109c08188016108b0565b9450505050509250929050565b60008082840360608112156109e157600080fd5b60408112156109ef57600080fd5b506040516040810181811067ffffffffffffffff82111715610a1357610a13610840565b604052835181526020840151610a288161080b565b60208201526040840151909250610a3e8161080b565b809150509250929050565b60208082526029908201527f54656c65706f7274657252656769737472793a207a65726f2070726f746f636f6040820152686c206164647265737360b81b606082015260800190565b8082018082111561017e57634e487b7160e01b600052601160045260246000fdfea2646970667358221220147aa4bf673206f63959dca6bf01bb7ab5e23e6ff9c146a03a27caed9a8296ef64736f6c63430008120033 diff --git a/pkg/teleporter/genesis/genesis.go b/pkg/teleporter/genesis/genesis.go new file mode 100644 index 000000000..2afbc67f9 --- /dev/null +++ b/pkg/teleporter/genesis/genesis.go @@ -0,0 +1,129 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package genesis + +import ( + _ "embed" + "encoding/hex" + "fmt" + "math/big" + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/subnet-evm/core" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + messengerVersion = "0x1" + MessengerContractAddress = "0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf" + RegistryContractAddress = "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228" + MessengerDeployerAddress = "0x618FEdD9A45a8C456812ecAAE70C671c6249DfaC" +) + +//go:embed deployed_messenger_bytecode.txt +var deployedMessengerBytecode []byte + +//go:embed deployed_registry_bytecode.txt +var deployedRegistryBytecode []byte + +func setSimpleStorageValue( + storage map[common.Hash]common.Hash, + slot string, + value string, +) { + storage[common.HexToHash(slot)] = common.HexToHash(value) +} + +func hexFill32(s string) string { + return fmt.Sprintf("%064s", utils.TrimHexa(s)) +} + +func setMappingStorageValue( + storage map[common.Hash]common.Hash, + slot string, + key string, + value string, +) error { + slot = hexFill32(slot) + key = hexFill32(key) + storageKey := key + slot + storageKeyBytes, err := hex.DecodeString(storageKey) + if err != nil { + return err + } + storage[crypto.Keccak256Hash(storageKeyBytes)] = common.HexToHash(value) + return nil +} + +func AddICMMessengerContractToAllocations( + allocs core.GenesisAlloc, +) { + const ( + blockchainIDSlot = "0x0" + messageNonceSlot = "0x1" + ) + storage := map[common.Hash]common.Hash{} + setSimpleStorageValue(storage, blockchainIDSlot, "0x1") + setSimpleStorageValue(storage, messageNonceSlot, "0x1") + deployedMessengerBytes := common.FromHex(strings.TrimSpace(string(deployedMessengerBytecode))) + allocs[common.HexToAddress(MessengerContractAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedMessengerBytes, + Storage: storage, + Nonce: 1, + } + allocs[common.HexToAddress(MessengerDeployerAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Nonce: 1, + } +} + +func AddICMRegistryContractToAllocations( + allocs core.GenesisAlloc, +) error { + const ( + latestVersionSlot = "0x0" + versionToAddressSlot = "0x1" + addressToVersionSlot = "0x2" + ) + storage := map[common.Hash]common.Hash{} + setSimpleStorageValue(storage, latestVersionSlot, messengerVersion) + if err := setMappingStorageValue(storage, versionToAddressSlot, messengerVersion, MessengerContractAddress); err != nil { + return err + } + if err := setMappingStorageValue(storage, addressToVersionSlot, MessengerContractAddress, messengerVersion); err != nil { + return err + } + deployedRegistryBytes := common.FromHex(strings.TrimSpace(string(deployedRegistryBytecode))) + allocs[common.HexToAddress(RegistryContractAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedRegistryBytes, + Storage: storage, + Nonce: 1, + } + return nil +} + +// check if [genesisData] has +// smart contracts (len(alloc.Code)>0) allocated for +// ICM Messenger and ICM registry, +// based on their expected addresses [MessengerContractAddress] and +// [RegistryContractAddress] +// to be used by local blockchain deploy to determine if a teleporter deploy +// or a registry deploy is needed +func ICMAtGenesis( + genesisData []byte, +) (bool, bool, error) { + messengerAtGenesis, err := contract.ContractAddressIsInGenesisData(genesisData, common.HexToAddress(MessengerContractAddress)) + if err != nil { + return false, false, err + } + registryAtGenesis, err := contract.ContractAddressIsInGenesisData(genesisData, common.HexToAddress(RegistryContractAddress)) + if err != nil { + return false, false, err + } + return messengerAtGenesis, registryAtGenesis, nil +} diff --git a/pkg/teleporter/operate.go b/pkg/teleporter/operate.go index e037cfbfc..57a7b52ca 100644 --- a/pkg/teleporter/operate.go +++ b/pkg/teleporter/operate.go @@ -91,6 +91,8 @@ func SendCrossChainMessage( privateKey, messengerAddress, nil, + "send cross chain message", + nil, "sendCrossChainMessage((bytes32, address, (address, uint256), uint256, [address], bytes))->(bytes32)", params, ) diff --git a/pkg/teleporter/relayer.go b/pkg/teleporter/relayer.go index bb381ca7e..1ea3f3923 100644 --- a/pkg/teleporter/relayer.go +++ b/pkg/teleporter/relayer.go @@ -24,14 +24,17 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/awm-relayer/config" + apiConfig "github.com/ava-labs/awm-relayer/config" offchainregistry "github.com/ava-labs/awm-relayer/messages/off-chain-registry" + "github.com/ava-labs/awm-relayer/relayer/config" ) const ( - localRelayerSetupTime = 2 * time.Second - localRelayerCheckPoolTime = 100 * time.Millisecond - localRelayerCheckTimeout = 3 * time.Second + localRelayerSetupTime = 2 * time.Second + localRelayerCheckPoolTime = 100 * time.Millisecond + localRelayerCheckTimeout = 3 * time.Second + defaultDBWriteIntervalSeconds = 10 + defaultSignatureCacheSize = 1024 * 1024 ) var teleporterRelayerRequiredBalance = big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(500)) // 500 AVAX @@ -352,11 +355,11 @@ func CreateBaseRelayerConfig( ) error { awmRelayerConfig := &config.Config{ LogLevel: logLevel, - PChainAPI: &config.APIConfig{ + PChainAPI: &apiConfig.APIConfig{ BaseURL: network.Endpoint, QueryParams: map[string]string{}, }, - InfoAPI: &config.APIConfig{ + InfoAPI: &apiConfig.APIConfig{ BaseURL: network.Endpoint, QueryParams: map[string]string{}, }, @@ -365,6 +368,8 @@ func CreateBaseRelayerConfig( SourceBlockchains: []*config.SourceBlockchain{}, DestinationBlockchains: []*config.DestinationBlockchain{}, MetricsPort: metricsPort, + DBWriteIntervalSeconds: defaultDBWriteIntervalSeconds, + SignatureCacheSize: defaultSignatureCacheSize, } return saveRelayerConfig(awmRelayerConfig, relayerConfigPath) } @@ -472,10 +477,10 @@ func addSourceToRelayerConfig( SubnetID: subnetID, BlockchainID: blockchainID, VM: config.EVM.String(), - RPCEndpoint: config.APIConfig{ + RPCEndpoint: apiConfig.APIConfig{ BaseURL: rpcEndpoint, }, - WSEndpoint: config.APIConfig{ + WSEndpoint: apiConfig.APIConfig{ BaseURL: wsEndpoint, }, MessageContracts: map[string]config.MessageProtocolConfig{ @@ -509,7 +514,7 @@ func addDestinationToRelayerConfig( SubnetID: subnetID, BlockchainID: blockchainID, VM: config.EVM.String(), - RPCEndpoint: config.APIConfig{ + RPCEndpoint: apiConfig.APIConfig{ BaseURL: rpcEndpoint, }, AccountPrivateKey: relayerFundedAddressKey, diff --git a/pkg/teleporter/teleporter.go b/pkg/teleporter/teleporter.go index 05f5ab288..c4d73481b 100644 --- a/pkg/teleporter/teleporter.go +++ b/pkg/teleporter/teleporter.go @@ -240,6 +240,7 @@ func (t *Deployer) Deploy( privateKey string, deployMessenger bool, deployRegistry bool, + forceRegistryDeploy bool, ) (bool, string, string, error) { var ( messengerAddress string @@ -255,7 +256,7 @@ func (t *Deployer) Deploy( ) } if err == nil && deployRegistry { - if !deployMessenger || !alreadyDeployed { + if !deployMessenger || !alreadyDeployed || forceRegistryDeploy { registryAddress, err = t.DeployRegistry(subnetName, rpcURL, privateKey) } } @@ -404,24 +405,23 @@ func DeployAndFundRelayer( privKeyStr, true, true, + true, ) if err != nil { return false, "", "", err } - if !alreadyDeployed { - // get relayer address - relayerAddress, _, err := GetRelayerKeyInfo(app.GetKeyPath(constants.AWMRelayerKeyName)) - if err != nil { - return false, "", "", err - } - // fund relayer - if err := FundRelayer( - endpoint, - privKeyStr, - relayerAddress, - ); err != nil { - return false, "", "", err - } + // get relayer address + relayerAddress, _, err := GetRelayerKeyInfo(app.GetKeyPath(constants.AWMRelayerKeyName)) + if err != nil { + return false, "", "", err + } + // fund relayer + if err := FundRelayer( + endpoint, + privKeyStr, + relayerAddress, + ); err != nil { + return false, "", "", err } return alreadyDeployed, messengerAddress, registryAddress, err } diff --git a/pkg/txutils/auth.go b/pkg/txutils/auth.go index e00e84e6b..0c05081fc 100644 --- a/pkg/txutils/auth.go +++ b/pkg/txutils/auth.go @@ -37,6 +37,8 @@ func GetAuthSigners(tx *txs.Tx, controlKeys []string) ([]string, error) { subnetAuth = unsignedTx.SubnetAuth case *txs.TransferSubnetOwnershipTx: subnetAuth = unsignedTx.SubnetAuth + case *txs.ConvertSubnetToL1Tx: + subnetAuth = unsignedTx.SubnetAuth default: return nil, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } diff --git a/pkg/txutils/info.go b/pkg/txutils/info.go index 52168bd3b..30bdae4da 100644 --- a/pkg/txutils/info.go +++ b/pkg/txutils/info.go @@ -31,6 +31,8 @@ func GetNetwork(tx *txs.Tx) (models.Network, error) { networkID = unsignedTx.NetworkID case *txs.TransferSubnetOwnershipTx: networkID = unsignedTx.NetworkID + case *txs.ConvertSubnetToL1Tx: + networkID = unsignedTx.NetworkID default: return models.UndefinedNetwork, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } @@ -58,6 +60,8 @@ func GetSubnetID(tx *txs.Tx) (ids.ID, error) { subnetID = unsignedTx.Subnet case *txs.TransferSubnetOwnershipTx: subnetID = unsignedTx.Subnet + case *txs.ConvertSubnetToL1Tx: + subnetID = unsignedTx.Subnet default: return ids.Empty, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 00d34c24c..2ca53b546 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -493,37 +493,6 @@ func FileIsSubnetEVMGenesis(genesisPath string) (bool, error) { return ByteSliceIsSubnetEvmGenesis(genesisBytes), nil } -func GetKeyNames(keyDir string, addEwoq bool) ([]string, error) { - matches, err := os.ReadDir(keyDir) - if err != nil { - return nil, err - } - var names []string - for _, m := range matches { - if strings.HasSuffix(m.Name(), constants.KeySuffix) { - names = append(names, strings.TrimSuffix(m.Name(), constants.KeySuffix)) - } - } - userKeys := []string{} - cliKeys := []string{} - subnetKeys := []string{} - for _, keyName := range names { - switch { - case strings.HasPrefix(keyName, "cli-"): - cliKeys = append(cliKeys, keyName) - case strings.HasPrefix(keyName, "subnet_"): - subnetKeys = append(subnetKeys, keyName) - default: - userKeys = append(userKeys, keyName) - } - } - if addEwoq { - userKeys = append(userKeys, "ewoq") - } - names = append(append(userKeys, subnetKeys...), cliKeys...) - return names, nil -} - func GetDefaultBlockchainAirdropKeyName(blockchainName string) string { return "subnet_" + blockchainName + "_airdrop" } diff --git a/pkg/utils/common_test.go b/pkg/utils/common_test.go index 97439ed25..34a427582 100644 --- a/pkg/utils/common_test.go +++ b/pkg/utils/common_test.go @@ -3,10 +3,13 @@ package utils import ( + "encoding/json" "errors" "reflect" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestSplitKeyValueStringToMap(t *testing.T) { @@ -293,3 +296,88 @@ func TestRetryFunction(t *testing.T) { t.Errorf("Expected nil result, got %v", result) } } + +func TestSetJSONKey(t *testing.T) { + tests := []struct { + desc string + json string + k string + v interface{} + shouldErr bool + out string + }{ + { + desc: "invalid json", + json: "", + k: "k", + v: "v", + shouldErr: true, + out: "", + }, + { + desc: "empty json", + json: "{}", + k: "k", + v: "v", + shouldErr: false, + out: "{\"k\": \"v\"}", + }, + { + desc: "remove value", + json: "{\"k\": \"v\"}", + k: "k", + v: nil, + shouldErr: false, + out: "{}", + }, + { + desc: "remove value on empty", + json: "{}", + k: "k", + v: nil, + shouldErr: false, + out: "{}", + }, + { + desc: "remove value on multiple", + json: "{\"k\": \"v\", \"k2\": \"v2\"}", + k: "k", + v: nil, + shouldErr: false, + out: "{\"k2\": \"v2\"}", + }, + { + desc: "change value", + json: "{\"k\": \"v\"}", + k: "k", + v: "newv", + shouldErr: false, + out: "{\"k\": \"newv\"}", + }, + { + desc: "change value on multiple", + json: "{\"k\": \"v\", \"k2\": \"v2\"}", + k: "k", + v: "v1", + shouldErr: false, + out: "{\"k\": \"v1\", \"k2\": \"v2\"}", + }, + } + + require := require.New(t) + for _, test := range tests { + out, err := SetJSONKey(test.json, test.k, test.v) + if test.shouldErr { + require.Error(err, test.desc) + } else { + require.NoError(err, test.desc) + var expectedOutMap map[string]interface{} + var outMap map[string]interface{} + err := json.Unmarshal([]byte(out), &outMap) + require.NoError(err, test.desc) + err = json.Unmarshal([]byte(test.out), &expectedOutMap) + require.NoError(err, test.desc) + require.Equal(expectedOutMap, outMap, test.desc) + } + } +} diff --git a/pkg/utils/json.go b/pkg/utils/json.go index d35b7eb30..b2d6d9eac 100644 --- a/pkg/utils/json.go +++ b/pkg/utils/json.go @@ -25,3 +25,36 @@ func ValidateJSON(path string) ([]byte, error) { return contentBytes, nil } + +// ReadJSON takes a json string and returns its associated map +// if it contains valid JSON +func ReadJSON(path string) (map[string]interface{}, error) { + var content map[string]interface{} + contentBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if err := json.Unmarshal(contentBytes, &content); err != nil { + return nil, fmt.Errorf("this looks like invalid JSON: %w", err) + } + return content, nil +} + +// Set k=v in JSON string +// e.g., "track-subnets" is the key and value is "a,b,c". +func SetJSONKey(jsonBody string, k string, v interface{}) (string, error) { + var config map[string]interface{} + if err := json.Unmarshal([]byte(jsonBody), &config); err != nil { + return "", err + } + if v == nil { + delete(config, k) + } else { + config[k] = v + } + updatedJSON, err := json.Marshal(config) + if err != nil { + return "", err + } + return string(updatedJSON), nil +} diff --git a/pkg/utils/keys.go b/pkg/utils/keys.go new file mode 100644 index 000000000..102fca95f --- /dev/null +++ b/pkg/utils/keys.go @@ -0,0 +1,55 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package utils + +import ( + "os" + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm" +) + +func GetKeyNames(keyDir string, addEwoq bool) ([]string, error) { + matches, err := os.ReadDir(keyDir) + if err != nil { + return nil, err + } + var names []string + for _, m := range matches { + if strings.HasSuffix(m.Name(), constants.KeySuffix) { + names = append(names, strings.TrimSuffix(m.Name(), constants.KeySuffix)) + } + } + userKeys := []string{} + cliKeys := []string{} + subnetKeys := []string{} + for _, keyName := range names { + switch { + case strings.HasPrefix(keyName, "cli-"): + cliKeys = append(cliKeys, keyName) + case strings.HasPrefix(keyName, "subnet_"): + subnetKeys = append(subnetKeys, keyName) + default: + userKeys = append(userKeys, keyName) + } + } + if addEwoq { + userKeys = append(userKeys, "ewoq") + } + names = append(append(userKeys, subnetKeys...), cliKeys...) + return names, nil +} + +func GetNetworkBalance(addressList []ids.ShortID, networkEndpoint string) (uint64, error) { + ctx, cancel := GetAPIContext() + defer cancel() + pClient := platformvm.NewClient(networkEndpoint) + bal, err := pClient.GetBalance(ctx, addressList) + if err != nil { + return 0, err + } + return uint64(bal.Balance) / units.Avax, nil +} diff --git a/pkg/utils/net.go b/pkg/utils/net.go index 99dad17a2..408a79627 100644 --- a/pkg/utils/net.go +++ b/pkg/utils/net.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/netip" "net/url" ) @@ -56,3 +57,11 @@ func IsValidURL(urlString string) bool { } return true } + +// IsValidIPPort checks if an string IP:port pair is valid. +func IsValidIPPort(ipPortPair string) bool { + if _, err := netip.ParseAddrPort(ipPortPair); err != nil { + return false + } + return true +} diff --git a/pkg/utils/net_test.go b/pkg/utils/net_test.go new file mode 100644 index 000000000..57a9d2a5b --- /dev/null +++ b/pkg/utils/net_test.go @@ -0,0 +1,31 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package utils + +import ( + "testing" +) + +func TestIsValidIPPort(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"127.0.0.1:8080", true}, // valid IP:port + {"256.0.0.1:8080", false}, // invalid IP address + {"example.com:8080", false}, // only ip address is allowed + {"127.0.0.1", false}, // missing port + {"[::1]:8080", true}, // valid IPv6 address + {"[::1]", false}, // missing port for IPv6 + {"", false}, // empty string + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := IsValidIPPort(test.input) + if result != test.expected { + t.Errorf("Expected IsValidIPPort(%s) to be %v, but got %v", test.input, test.expected, result) + } + }) + } +} diff --git a/pkg/utils/staking.go b/pkg/utils/staking.go index ecd16fedb..5df9d88a6 100644 --- a/pkg/utils/staking.go +++ b/pkg/utils/staking.go @@ -5,10 +5,14 @@ package utils import ( "encoding/pem" "fmt" + "os" + "path/filepath" + "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/staking" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" ) func NewBlsSecretKeyBytes() ([]byte, error) { @@ -30,3 +34,42 @@ func ToNodeID(certBytes []byte) (ids.NodeID, error) { } return ids.NodeIDFromCert(cert), nil } + +func ToBLSPoP(keyBytes []byte) ( + []byte, // bls public key + []byte, // bls proof of possession + error, +) { + sk, err := bls.SecretKeyFromBytes(keyBytes) + if err != nil { + return nil, nil, err + } + pop := signer.NewProofOfPossession(sk) + return pop.PublicKey[:], pop.ProofOfPossession[:], nil +} + +// GetNodeParams returns node id, bls public key and bls proof of possession +func GetNodeParams(nodeDir string) ( + ids.NodeID, + []byte, // bls public key + []byte, // bls proof of possession + error, +) { + certBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.StakerCertFileName)) + if err != nil { + return ids.EmptyNodeID, nil, nil, err + } + nodeID, err := ToNodeID(certBytes) + if err != nil { + return ids.EmptyNodeID, nil, nil, err + } + blsKeyBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.BLSKeyFileName)) + if err != nil { + return ids.EmptyNodeID, nil, nil, err + } + blsPub, blsPoP, err := ToBLSPoP(blsKeyBytes) + if err != nil { + return ids.EmptyNodeID, nil, nil, err + } + return nodeID, blsPub, blsPoP, nil +} diff --git a/pkg/utils/strings.go b/pkg/utils/strings.go index a6ff8cb34..ad78d1377 100644 --- a/pkg/utils/strings.go +++ b/pkg/utils/strings.go @@ -71,3 +71,8 @@ func FormatAmount(amount *big.Int, decimals uint8) string { val := new(big.Float).Quo(amountFloat, divisor) return fmt.Sprintf("%.*f", decimals, val) } + +// Removes the leading 0x/0X part of a hexadecimal string representation +func TrimHexa(s string) string { + return strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X") +} diff --git a/pkg/ux/progressbar.go b/pkg/ux/progressbar.go new file mode 100644 index 000000000..90362654b --- /dev/null +++ b/pkg/ux/progressbar.go @@ -0,0 +1,47 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package ux + +import ( + "fmt" + "time" + + ansi "github.com/k0kubun/go-ansi" + progressbar "github.com/schollz/progressbar/v3" +) + +func TimedProgressBar( + duration time.Duration, + title string, + extraSteps int, +) (*progressbar.ProgressBar, error) { + const steps = 1000 + stepDuration := duration / steps + bar := progressbar.NewOptions(steps+extraSteps, + progressbar.OptionSetWriter(ansi.NewAnsiStdout()), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionSetElapsedTime(false), + progressbar.OptionSetWidth(15), + progressbar.OptionSetDescription(title), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]=[reset]", + SaucerHead: "[green]>[reset]", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + })) + for i := 0; i < steps; i++ { + if err := bar.Add(1); err != nil { + return nil, err + } + time.Sleep(stepDuration) + } + if extraSteps == 0 { + fmt.Println() + } + return bar, nil +} + +func ExtraStepExecuted(bar *progressbar.ProgressBar) error { + return bar.Add(1) +} diff --git a/pkg/ux/spinner.go b/pkg/ux/spinner.go index 49e3f732e..bd35b11f6 100644 --- a/pkg/ux/spinner.go +++ b/pkg/ux/spinner.go @@ -11,6 +11,7 @@ import ( "github.com/chelnak/ysmrr" "github.com/chelnak/ysmrr/pkg/animations" "github.com/chelnak/ysmrr/pkg/colors" + ansi "github.com/k0kubun/go-ansi" ) type UserSpinner struct { @@ -43,7 +44,9 @@ func (us *UserSpinner) Stop() { func (us *UserSpinner) SpinToUser(msg string, args ...interface{}) *ysmrr.Spinner { formattedMsg := fmt.Sprintf(msg, args...) - Logger.log.Info(formattedMsg + " [Spinner Start]") + if Logger != nil { + Logger.log.Info(formattedMsg + " [Spinner Start]") + } sp := us.spinner.AddSpinner(formattedMsg) us.mutex.Lock() if !us.started { @@ -55,6 +58,7 @@ func (us *UserSpinner) SpinToUser(msg string, args ...interface{}) *ysmrr.Spinne } func SpinFailWithError(s *ysmrr.Spinner, txt string, err error) { + ansi.CursorShow() if txt == "" { s.ErrorWithMessagef("%s err:%v", s.GetMessage(), err) } else { @@ -64,9 +68,12 @@ func SpinFailWithError(s *ysmrr.Spinner, txt string, err error) { } func SpinComplete(s *ysmrr.Spinner) { + ansi.CursorShow() if s.IsComplete() { return } s.Complete() - Logger.log.Info(s.GetMessage() + " [Spinner Complete]") + if Logger != nil { + Logger.log.Info(s.GetMessage() + " [Spinner Complete]") + } } diff --git a/pkg/validatormanager/deployed_native_pos_validator_manager_bytecode.txt b/pkg/validatormanager/deployed_native_pos_validator_manager_bytecode.txt new file mode 100644 index 000000000..9134bd039 --- /dev/null +++ b/pkg/validatormanager/deployed_native_pos_validator_manager_bytecode.txt @@ -0,0 +1 @@ +0x6080604052600436106101f1575f3560e01c80638280a25a11610108578063ba3a4b971161009d578063c599e24f1161006d578063c599e24f1461058d578063c974d1b6146105a0578063d5f20ff6146105b4578063df93d8de146105e0578063fd7ac5e7146105f6575f80fd5b8063ba3a4b9714610510578063bc5fbfec1461052f578063bee0a03f1461054f578063c257a0f51461056e575f80fd5b8063a9778a7a116100d8578063a9778a7a14610326578063af2f5feb146104c3578063afb98096146104d6578063b771b3bc146104f6575f80fd5b80638280a25a1461045257806393e245981461046657806398f3e2b414610485578063a3a65e48146104a4575f80fd5b806335455ded1161018957806360305d621161015957806360305d62146103ab57806362065856146103d457806366435abf14610401578063732214f81461042057806376f7862114610433575f80fd5b806335455ded146103265780633a1cfff61461034e578063467ef06f1461036d5780635297fae61461038c575f80fd5b806320d91b7a116101c457806320d91b7a1461027f57806325e1c7761461029e5780632e2194d8146102bd578063329c3e12146102f4575f80fd5b80630118acc4146101f55780630322ed9814610216578063151d30d1146102355780631ec4472414610260575b5f80fd5b348015610200575f80fd5b5061021461020f366004615021565b610615565b005b348015610221575f80fd5b5061021461023036600461505c565b61064a565b348015610240575f80fd5b50610249600a81565b60405160ff90911681526020015b60405180910390f35b34801561026b575f80fd5b5061021461027a366004615021565b610858565b34801561028a575f80fd5b50610214610299366004615073565b610863565b3480156102a9575f80fd5b506102146102b83660046150c1565b610d8d565b3480156102c8575f80fd5b506102dc6102d736600461505c565b610e01565b6040516001600160401b039091168152602001610257565b3480156102ff575f80fd5b5061030e6001600160991b0181565b6040516001600160a01b039091168152602001610257565b348015610331575f80fd5b5061033b61271081565b60405161ffff9091168152602001610257565b348015610359575f80fd5b50610214610368366004615021565b610e5b565b348015610378575f80fd5b506102146103873660046150e2565b610e66565b348015610397575f80fd5b506102146103a63660046150fb565b610f27565b3480156103b6575f80fd5b506103bf601481565b60405163ffffffff9091168152602001610257565b3480156103df575f80fd5b506103f36103ee366004615139565b611192565b604051908152602001610257565b34801561040c575f80fd5b506102dc61041b36600461505c565b6111c9565b34801561042b575f80fd5b506103f35f81565b34801561043e575f80fd5b5061021461044d366004615021565b6111dd565b34801561045d575f80fd5b50610249603081565b348015610471575f80fd5b5061021461048036600461505c565b611208565b348015610490575f80fd5b5061021461049f3660046150fb565b6112d3565b3480156104af575f80fd5b506102146104be3660046150e2565b6114ab565b6103f36104d1366004615163565b611634565b3480156104e1575f80fd5b506103f35f80516020615cbe83398151915281565b348015610501575f80fd5b5061030e6005600160991b0181565b34801561051b575f80fd5b5061021461052a36600461505c565b611668565b34801561053a575f80fd5b506103f35f80516020615cde83398151915281565b34801561055a575f80fd5b5061021461056936600461505c565b611851565b348015610579575f80fd5b506102146105883660046151b7565b61198e565b6103f361059b36600461505c565b611a6b565b3480156105ab575f80fd5b50610249601481565b3480156105bf575f80fd5b506105d36105ce36600461505c565b611a9c565b6040516102579190615243565b3480156105eb575f80fd5b506102dc6202a30081565b348015610601575f80fd5b506103f36106103660046152c3565b611beb565b610620838383611c46565b61064557604051631036cf9160e11b8152600481018490526024015b60405180910390fd5b505050565b5f8181525f80516020615d3e8339815191526020526040808220815160e0810190925280545f80516020615cde83398151915293929190829060ff166005811115610697576106976151ce565b60058111156106a8576106a86151ce565b81526020016001820180546106bc9061532e565b80601f01602080910402602001604051908101604052809291908181526020018280546106e89061532e565b80156107335780601f1061070a57610100808354040283529160200191610733565b820191905f5260205f20905b81548152906001019060200180831161071657829003601f168201915b505050918352505060028201546001600160401b038082166020840152600160401b820481166040840152600160801b820481166060840152600160c01b909104811660808301526003928301541660a0909101529091508151600581111561079e5761079e6151ce565b146107d1575f8381526007830160205260409081902054905163170cc93360e21b815261063c9160ff1690600401615360565b6005600160991b016001600160a01b031663ee5b48eb6107f68584606001515f611f97565b6040518263ffffffff1660e01b8152600401610812919061536e565b6020604051808303815f875af115801561082e573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906108529190615380565b50505050565b610852838383611c46565b7fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb09545f80516020615cde8339815191529060ff16156108b557604051637fab81e560e01b815260040160405180910390fd5b6005600160991b016001600160a01b0316634213cf786040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108f8573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061091c9190615380565b836020013514610945576040516372b0a7e760e11b81526020840135600482015260240161063c565b3061095660608501604086016153ab565b6001600160a01b0316146109995761097460608401604085016153ab565b604051632f88120d60e21b81526001600160a01b03909116600482015260240161063c565b5f6109a760608501856153c6565b905090505f805b828163ffffffff161015610c8f575f6109ca60608801886153c6565b8363ffffffff168181106109e0576109e061540b565b90506020028101906109f2919061541f565b6109fb90615543565b80516040519192505f916008880191610a13916155bc565b90815260200160405180910390205414610a4357805160405163a41f772f60e01b815261063c919060040161536e565b5f6002885f013584604051602001610a7292919091825260e01b6001600160e01b031916602082015260240190565b60408051601f1981840301815290829052610a8c916155bc565b602060405180830381855afa158015610aa7573d5f803e3d5ffd5b5050506040513d601f19601f82011682018060405250810190610aca9190615380565b90508086600801835f0151604051610ae291906155bc565b90815260408051602092819003830181209390935560e0830181526002835284518284015284810180516001600160401b03908116858401525f60608601819052915181166080860152421660a085015260c0840181905284815260078a01909252902081518154829060ff19166001836005811115610b6457610b646151ce565b021790555060208201516001820190610b7d9082615611565b506040828101516002830180546060860151608087015160a08801516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909301516003909201805467ffffffffffffffff191692841692909217909155830151610c229116856156e0565b8251604051919550610c33916155bc565b60408051918290038220908401516001600160401b031682529082907f9d47fef9da077661546e646d61830bfcbda90506c2e5eed38195e82c4eb1cbdf9060200160405180910390a3505080610c88906156f3565b90506109ae565b50600483018190556001830154606490610cb390600160401b900460ff1683615715565b1015610cd557604051635943317f60e01b81526004810182905260240161063c565b5f610ceb610ce286611fe6565b604001516120fc565b90505f610cf787612289565b90505f600282604051610d0a91906155bc565b602060405180830381855afa158015610d25573d5f803e3d5ffd5b5050506040513d601f19601f82011682018060405250810190610d489190615380565b9050828114610d7457604051631872fc8d60e01b8152600481018290526024810184905260440161063c565b5050506009909201805460ff1916600117905550505050565b610d9682612463565b610db6576040516330efa98b60e01b81526004810183905260240161063c565b5f610dc083611a9c565b5190506002816005811115610dd757610dd76151ce565b14610df7578060405163170cc93360e21b815260040161063c9190615360565b610852838361249e565b5f805f80516020615cbe83398151915260030154610e1f908461572c565b9050801580610e3457506001600160401b0381115b15610e555760405163222d164360e21b81526004810184905260240161063c565b92915050565b610852838383612718565b610e6e612904565b5f80516020615cbe8339815191525f80610e878461293b565b91509150610e9482612463565b610ea057505050610f0e565b5f8281526005840160205260409020546001600160a01b0316600482516005811115610ece57610ece6151ce565b03610ef3575f83815260088501602052604081208054919055610ef18282612c79565b505b610f0981610f048460400151611192565b612cd7565b505050505b610f2460015f80516020615d1e83398151915255565b50565b5f8181525f80516020615cfe8339815191526020526040808220815160e0810190925280545f80516020615cbe83398151915293929190829060ff166003811115610f7457610f746151ce565b6003811115610f8557610f856151ce565b8152815461010090046001600160a01b0316602082015260018201546040808301919091526002909201546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c0909101528101519091505f610ffb82611a9c565b9050600183516003811115611012576110126151ce565b14611033578251604051633b0d540d60e21b815261063c919060040161574b565b600481516005811115611048576110486151ce565b0361105e5761105685612cfd565b505050505050565b5f8061107561106c89611fe6565b60400151612f51565b50915091508184146110a257846040015160405163089938b360e11b815260040161063c91815260200190565b806001600160401b031683606001516001600160401b031610806110db5750806001600160401b03168560a001516001600160401b0316115b1561110457604051632e19bc2d60e11b81526001600160401b038216600482015260240161063c565b5f878152600687016020908152604091829020805460ff1916600290811782550180546001600160401b034216600160401b81026fffffffffffffffff000000000000000019909216919091179091559151918252859189917f047059b465069b8b751836b41f9f1d83daff583d2238cc7fbb461437ec23a4f6910160405180910390a35050505050505050565b7f4317713f7ecbdddd4bc99e95d903adedaa883b2e7c2551610bd13e2c7e473d03545f90610e55906001600160401b038416615715565b5f6111d382611a9c565b6080015192915050565b6111e8838383612718565b61064557604051635bff683f60e11b81526004810184905260240161063c565b5f80516020615cbe8339815191525f61122083611a9c565b5190506004816005811115611237576112376151ce565b14611257578060405163170cc93360e21b815260040161063c9190615360565b5f8381526005830160205260409020546001600160a01b0316331461129d57335b604051636e2ccd7560e11b81526001600160a01b03909116600482015260240161063c565b5f83815260088301602090815260408083208054908490556005860190925290912054610852906001600160a01b031682612c79565b6112db612904565b5f8181525f80516020615cfe8339815191526020526040808220815160e0810190925280545f80516020615cbe83398151915293929190829060ff166003811115611328576113286151ce565b6003811115611339576113396151ce565b8152815461010090046001600160a01b03166020820152600182015460408201526002909101546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c09091015290506003815160038111156113b2576113b26151ce565b146113d3578051604051633b0d540d60e21b815261063c919060040161574b565b60046113e28260400151611a9c565b5160058111156113f4576113f46151ce565b14611486575f61140385611fe6565b90505f806114148360400151612f51565b5091509150818460400151146114405760405163089938b360e11b81526004810183905260240161063c565b806001600160401b03168460c001516001600160401b0316111561148257604051632e19bc2d60e11b81526001600160401b038216600482015260240161063c565b5050505b61148f83612cfd565b50506114a760015f80516020615d1e83398151915255565b5050565b5f80516020615cde8339815191525f806114d06114c785611fe6565b604001516131a7565b91509150806114f657604051632d07135360e01b8152811515600482015260240161063c565b5f828152600684016020526040902080546115109061532e565b90505f036115345760405163089938b360e11b81526004810183905260240161063c565b60015f83815260078501602052604090205460ff16600581111561155a5761155a6151ce565b1461158d575f8281526007840160205260409081902054905163170cc93360e21b815261063c9160ff1690600401615360565b5f82815260068401602052604081206115a591614f75565b5f828152600784016020908152604091829020805460ff1916600290811782550180546001600160401b0342818116600160c01b026001600160c01b0390931692909217928390558451600160801b9093041682529181019190915283917ff8fd1c90fb9cfa2ca2358fdf5806b086ad43315d92b221c929efc7f105ce7568910160405180910390a250505050565b5f61163d612904565b61164984848434613363565b905061166160015f80516020615d1e83398151915255565b9392505050565b5f8181525f80516020615cfe8339815191526020526040808220815160e0810190925280545f80516020615cbe83398151915293929190829060ff1660038111156116b5576116b56151ce565b60038111156116c6576116c66151ce565b8152815461010090046001600160a01b0316602082015260018083015460408301526002909201546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c0909101529091508151600381111561173f5761173f6151ce565b14158015611760575060038151600381111561175d5761175d6151ce565b14155b15611781578051604051633b0d540d60e21b815261063c919060040161574b565b5f61178f8260400151611a9c565b905080606001516001600160401b03165f036117c1576040516339b894f960e21b81526004810185905260240161063c565b6005600160991b016001600160a01b031663ee5b48eb6117ee846040015184606001518560800151611f97565b6040518263ffffffff1660e01b815260040161180a919061536e565b6020604051808303815f875af1158015611826573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061184a9190615380565b5050505050565b5f8181527fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb066020526040902080545f80516020615cde83398151915291906118989061532e565b90505f036118bc5760405163089938b360e11b81526004810183905260240161063c565b60015f83815260078301602052604090205460ff1660058111156118e2576118e26151ce565b14611915575f8281526007820160205260409081902054905163170cc93360e21b815261063c9160ff1690600401615360565b5f82815260068201602052604090819020905163ee5b48eb60e01b81526005600160991b019163ee5b48eb9161194e9190600401615765565b6020604051808303815f875af115801561196a573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906106459190615380565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805460029190600160401b900460ff16806119d7575080546001600160401b03808416911610155b156119f55760405163f92ee8a960e01b815260040160405180910390fd5b805468ffffffffffffffffff19166001600160401b03831617600160401b178155611a1f83613531565b805460ff60401b191681556040516001600160401b03831681527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a1505050565b5f611a74612904565b611a7f823334613542565b9050611a9760015f80516020615d1e83398151915255565b919050565b611aa4614fac565b5f8281525f80516020615d3e833981519152602052604090819020815160e0810190925280545f80516020615cde833981519152929190829060ff166005811115611af157611af16151ce565b6005811115611b0257611b026151ce565b8152602001600182018054611b169061532e565b80601f0160208091040260200160405190810160405280929190818152602001828054611b429061532e565b8015611b8d5780601f10611b6457610100808354040283529160200191611b8d565b820191905f5260205f20905b815481529060010190602001808311611b7057829003601f168201915b505050918352505060028201546001600160401b038082166020840152600160401b820481166040840152600160801b820481166060840152600160c01b9091048116608083015260039092015490911660a0909101529392505050565b6040515f905f80516020615cde833981519152907fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb0890611c2e90869086906157ef565b90815260200160405180910390205491505092915050565b5f8381525f80516020615cfe8339815191526020526040808220815160e0810190925280545f80516020615cbe8339815191529284929091829060ff166003811115611c9457611c946151ce565b6003811115611ca557611ca56151ce565b8152815461010090046001600160a01b0316602082015260018201546040808301919091526002909201546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c0909101528101519091505f611d1b82611a9c565b9050600283516003811115611d3257611d326151ce565b14611d53578251604051633b0d540d60e21b815261063c919060040161574b565b60208301516001600160a01b03163314611def575f8281526005850160205260409020546001600160a01b03163314611d8c5733611278565b5f82815260058501602052604090205460a0820151611dbb91600160b01b90046001600160401b0316906157fe565b6001600160401b0316421015611def5760405163fb6ce63f60e01b81526001600160401b034216600482015260240161063c565b600281516005811115611e0457611e046151ce565b03611f325760028401546080840151611e26916001600160401b0316906157fe565b6001600160401b0316421015611e5a5760405163fb6ce63f60e01b81526001600160401b034216600482015260240161063c565b8615611e6c57611e6a828761249e565b505b5f8881526006850160205260409020805460ff1916600317905560608301516080820151611ea5918491611ea0919061581e565b613817565b505f898152600686016020526040812060020180546001600160401b03909316600160c01b026001600160c01b0390931692909217909155611ee68461396e565b5f8a81526007870160205260408082208390555191925084918b917f366d336c0ab380dc799f095a6f82a26326585c52909cc698b09ba4540709ed5791a3151594506116619350505050565b600481516005811115611f4757611f476151ce565b03611f7b57611f558361396e565b5f898152600786016020526040902055611f6e88612cfd565b6001945050505050611661565b805160405163170cc93360e21b815261063c9190600401615360565b604080515f6020820152600360e01b602282015260268101949094526001600160c01b031960c093841b811660468601529190921b16604e830152805180830360360181526056909201905290565b60408051606080820183525f8083526020830152918101919091526040516306f8253560e41b815263ffffffff831660048201525f9081906005600160991b0190636f825350906024015f60405180830381865afa15801561204a573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526120719190810190615849565b915091508061209357604051636b2f19e960e01b815260040160405180910390fd5b8151156120b9578151604051636ba589a560e01b8152600481019190915260240161063c565b60208201516001600160a01b0316156120f5576020820151604051624de75d60e31b81526001600160a01b03909116600482015260240161063c565b5092915050565b5f815160261461213157815160405163cc92daa160e01b815263ffffffff90911660048201526026602482015260440161063c565b5f805b60028110156121805761214881600161591b565b612153906008615715565b61ffff168482815181106121695761216961540b565b016020015160f81c901b9190911790600101612134565b5061ffff8116156121aa5760405163407b587360e01b815261ffff8216600482015260240161063c565b5f805b6004811015612205576121c181600361591b565b6121cc906008615715565b63ffffffff16856121de8360026156e0565b815181106121ee576121ee61540b565b016020015160f81c901b91909117906001016121ad565b5063ffffffff81161561222b57604051635b60892f60e01b815260040160405180910390fd5b5f805b60208110156122805761224281601f61591b565b61224d906008615715565b866122598360066156e0565b815181106122695761226961540b565b016020015160f81c901b919091179060010161222e565b50949350505050565b60605f808335602085013560146122a5878701604089016153ab565b6122b260608901896153c6565b60405160f09790971b6001600160f01b0319166020880152602287019590955250604285019290925260e090811b6001600160e01b0319908116606286015260609290921b6bffffffffffffffffffffffff191660668501529190911b16607a820152607e0160405160208183030381529060405290505f5b61233860608501856153c6565b90508110156120f5578161234f60608601866153c6565b8381811061235f5761235f61540b565b9050602002810190612371919061541f565b61237b908061592e565b905061238a60608701876153c6565b8481811061239a5761239a61540b565b90506020028101906123ac919061541f565b6123b6908061592e565b6123c360608901896153c6565b868181106123d3576123d361540b565b90506020028101906123e5919061541f565b6123f390602081019061592e565b61240060608b018b6153c6565b888181106124105761241061540b565b9050602002810190612422919061541f565b612433906060810190604001615139565b6040516020016124499796959493929190615970565b60408051601f19818403018152919052915060010161232b565b5f9081527f4317713f7ecbdddd4bc99e95d903adedaa883b2e7c2551610bd13e2c7e473d0560205260409020546001600160a01b0316151590565b6040516306f8253560e41b815263ffffffff821660048201525f90819081906005600160991b0190636f825350906024015f60405180830381865afa1580156124e9573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526125109190810190615849565b915091508061253257604051636b2f19e960e01b815260040160405180910390fd5b6005600160991b016001600160a01b0316634213cf786040518163ffffffff1660e01b8152600401602060405180830381865afa158015612575573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906125999190615380565b8251146125bf578151604051636ba589a560e01b8152600481019190915260240161063c565b60208201516001600160a01b0316156125fb576020820151604051624de75d60e31b81526001600160a01b03909116600482015260240161063c565b5f8061260a8460400151613b0b565b915091508187146126315760405163089938b360e11b81526004810188905260240161063c565b5f8781527f4317713f7ecbdddd4bc99e95d903adedaa883b2e7c2551610bd13e2c7e473d0560205260409020600101545f80516020615cbe833981519152906001600160401b0390811690831611156126ee575f888152600582016020908152604091829020600101805467ffffffffffffffff19166001600160401b038616908117909155915191825289917fec44148e8ff271f2d0bacef1142154abacb0abb3a29eb3eb50e2ca97e86d0435910160405180910390a261270d565b5f8881526005820160205260409020600101546001600160401b031691505b509695505050505050565b5f5f80516020615cbe8339815191528161273186613cfc565b905061273c86612463565b61274b57600192505050611661565b5f8681526005830160205260409020546001600160a01b031633146127705733611278565b5f86815260058301602052604090205460a082015161279f91600160b01b90046001600160401b0316906157fe565b6001600160401b03168160c001516001600160401b031610156127e65760c081015160405163fb6ce63f60e01b81526001600160401b03909116600482015260240161063c565b5f85156127fe576127f7878661249e565b905061281c565b505f8681526005830160205260409020600101546001600160401b03165b600483015460408301515f916001600160a01b031690634f22429f9061284190611192565b60a086015160c087015160405160e085901b6001600160e01b031916815260048101939093526001600160401b03918216602484018190526044840152811660648301528516608482015260a401602060405180830381865afa1580156128aa573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906128ce9190615380565b905080846008015f8a81526020019081526020015f205f8282546128f291906156e0565b90915550501515979650505050505050565b5f80516020615d1e83398151915280546001190161293557604051633ee5aeb560e01b815260040160405180910390fd5b60029055565b5f612944614fac565b5f80516020615cde8339815191525f806129606114c787611fe6565b91509150801561298757604051632d07135360e01b8152811515600482015260240161063c565b5f828152600784016020526040808220815160e081019092528054829060ff1660058111156129b8576129b86151ce565b60058111156129c9576129c96151ce565b81526020016001820180546129dd9061532e565b80601f0160208091040260200160405190810160405280929190818152602001828054612a099061532e565b8015612a545780601f10612a2b57610100808354040283529160200191612a54565b820191905f5260205f20905b815481529060010190602001808311612a3757829003601f168201915b505050918352505060028201546001600160401b038082166020840152600160401b820481166040840152600160801b820481166060840152600160c01b909104811660808301526003928301541660a09091015290915081516005811115612abf57612abf6151ce565b14158015612ae05750600181516005811115612add57612add6151ce565b14155b15612b0157805160405163170cc93360e21b815261063c9190600401615360565b600381516005811115612b1657612b166151ce565b03612b245760048152612b29565b600581525b836008018160200151604051612b3f91906155bc565b90815260408051602092819003830190205f908190558581526007870190925290208151815483929190829060ff19166001836005811115612b8357612b836151ce565b021790555060208201516001820190612b9c9082615611565b5060408201516002820180546060850151608086015160a08701516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909201516003909101805467ffffffffffffffff19169190921617905580516005811115612c4257612c426151ce565b60405184907f1c08e59656f1a18dc2da76826cdc52805c43e897a17c50faefb8ab3c1526cc16905f90a39196919550909350505050565b6040516327ad555d60e11b81526001600160a01b0383166004820152602481018290526001600160991b0190634f5aaaba906044015f604051808303815f87803b158015612cc5575f80fd5b505af1158015611056573d5f803e3d5ffd5b6114a76001600160a01b03831682613fe0565b60015f80516020615d1e83398151915255565b5f8181525f80516020615cfe8339815191526020526040808220815160e0810190925280545f80516020615cbe83398151915293929190829060ff166003811115612d4a57612d4a6151ce565b6003811115612d5b57612d5b6151ce565b8152815461010090046001600160a01b0316602082015260018201546040808301919091526002909201546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c090910152810151909150612df87fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb01546001600160401b031690565b8260800151612e0791906157fe565b6001600160401b0316421015612e3b5760405163fb6ce63f60e01b81526001600160401b034216600482015260240161063c565b5f848152600684016020908152604080832080546001600160a81b03191681556001810184905560020183905560078601909152812080549082905590808215612ef6575f84815260058701602052604090205461271090612ea890600160a01b900461ffff1685615715565b612eb2919061572c565b915081866008015f8681526020019081526020015f205f828254612ed691906156e0565b90915550612ee69050828461591b565b9050612ef6856020015182612c79565b612f0b8560200151610f048760600151611192565b6040805182815260208101849052859189917f8ececf510070c320d9a55323ffabe350e294ae505fc0c509dc5736da6f5cc993910160405180910390a350505050505050565b5f805f8351603614612f8857835160405163cc92daa160e01b815263ffffffff90911660048201526036602482015260440161063c565b5f805b6002811015612fd757612f9f81600161591b565b612faa906008615715565b61ffff16868281518110612fc057612fc061540b565b016020015160f81c901b9190911790600101612f8b565b5061ffff8116156130015760405163407b587360e01b815261ffff8216600482015260240161063c565b5f805b600481101561305c5761301881600361591b565b613023906008615715565b63ffffffff16876130358360026156e0565b815181106130455761304561540b565b016020015160f81c901b9190911790600101613004565b5063ffffffff811660031461308457604051635b60892f60e01b815260040160405180910390fd5b5f805b60208110156130d95761309b81601f61591b565b6130a6906008615715565b886130b28360066156e0565b815181106130c2576130c261540b565b016020015160f81c901b9190911790600101613087565b505f805b6008811015613138576130f181600761591b565b6130fc906008615715565b6001600160401b0316896131118360266156e0565b815181106131215761312161540b565b016020015160f81c901b91909117906001016130dd565b505f805b60088110156131975761315081600761591b565b61315b906008615715565b6001600160401b03168a61317083602e6156e0565b815181106131805761318061540b565b016020015160f81c901b919091179060010161313c565b5091989097509095509350505050565b5f8082516027146131dd57825160405163cc92daa160e01b815263ffffffff90911660048201526027602482015260440161063c565b5f805b600281101561322c576131f481600161591b565b6131ff906008615715565b61ffff168582815181106132155761321561540b565b016020015160f81c901b91909117906001016131e0565b5061ffff8116156132565760405163407b587360e01b815261ffff8216600482015260240161063c565b5f805b60048110156132b15761326d81600361591b565b613278906008615715565b63ffffffff168661328a8360026156e0565b8151811061329a5761329a61540b565b016020015160f81c901b9190911790600101613259565b5063ffffffff81166002146132d957604051635b60892f60e01b815260040160405180910390fd5b5f805b602081101561332e576132f081601f61591b565b6132fb906008615715565b876133078360066156e0565b815181106133175761331761540b565b016020015160f81c901b91909117906001016132dc565b505f866026815181106133435761334361540b565b016020015191976001600160f81b03199092161515965090945050505050565b7f4317713f7ecbdddd4bc99e95d903adedaa883b2e7c2551610bd13e2c7e473d02545f905f80516020615cbe83398151915290600160401b900461ffff90811690861610806133b7575061271061ffff8616115b156133db57604051635f12e6c360e11b815261ffff8616600482015260240161063c565b60028101546001600160401b039081169085161015613417576040516202a06d60e11b81526001600160401b038516600482015260240161063c565b80548310806134295750806001015483115b1561344a5760405163222d164360e21b81526004810184905260240161063c565b825f61345582610e01565b90505f6134628983614073565b905060405180608001604052806134763390565b6001600160a01b03908116825261ffff808c166020808501919091526001600160401b03808d166040808701919091525f60609687018190528881526005909b018352998a902086518154938801519b8801518316600160b01b0267ffffffffffffffff60b01b199c909516600160a01b026001600160b01b03199094169516949094179190911798909816178155910151600190910180549190951667ffffffffffffffff19909116179093555090915050949350505050565b6135396145eb565b610f2481614636565b5f5f80516020615cbe8339815191528161355b84610e01565b90505f61356787611a9c565b905061357287612463565b613592576040516330efa98b60e01b81526004810188905260240161063c565b6002815160058111156135a7576135a76151ce565b146135c857805160405163170cc93360e21b815261063c9190600401615360565b5f8282608001516135d991906157fe565b905083600201600a9054906101000a90046001600160401b0316826040015161360291906159d9565b6001600160401b0316816001600160401b0316111561363f57604051636d51fe0560e11b81526001600160401b038216600482015260240161063c565b5f8061364b8a84613817565b915091505f8a8360405160200161367992919091825260c01b6001600160c01b031916602082015260280190565b60408051601f19818403018152828252805160209091012060e08301909152915080600181526001600160a01b038c1660208083019190915260408083018f90526001600160401b03808b1660608501525f6080850181905290881660a085015260c090930183905284835260068b01909152902081518154829060ff1916600183600381111561370c5761370c6151ce565b02179055506020828101518254610100600160a81b0319166101006001600160a01b039283160217835560408085015160018501556060808601516002909501805460808089015160a08a015160c0909a01516001600160401b03998a166001600160801b031990941693909317600160401b918a1691909102176001600160801b0316600160801b998916999099026001600160c01b031698909817600160c01b91881691909102179055815189861681528a861694810194909452938b1690830152918101859052908c16918d9184917fb0024b263bc3a0b728a6edea50a69efa841189f8d32ee8af9d1c2b1a1a223426910160405180910390a49a9950505050505050505050565b5f8281525f80516020615d3e833981519152602052604081206002015481905f80516020615cde83398151915290600160801b90046001600160401b031661385f85826146aa565b5f613869876148cd565b5f8881526007850160205260408120600201805467ffffffffffffffff60801b1916600160801b6001600160401b038b16021790559091506005600160991b0163ee5b48eb6138b98a858b611f97565b6040518263ffffffff1660e01b81526004016138d5919061536e565b6020604051808303815f875af11580156138f1573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906139159190615380565b604080516001600160401b038a811682526020820184905282519394508516928b927f07de5ff35a674a8005e661f3333c907ca6333462808762d19dc7b3abb1a8c1df928290030190a3909450925050505b9250929050565b5f805f80516020615cbe83398151915290505f61398e8460400151611a9c565b90505f6003825160058111156139a6576139a66151ce565b14806139c457506004825160058111156139c2576139c26151ce565b145b156139d4575060c0810151613a11565b6002825160058111156139e9576139e96151ce565b036139f5575042613a11565b815160405163170cc93360e21b815261063c9190600401615360565b84608001516001600160401b0316816001600160401b031611613a3857505f949350505050565b600483015460608601516001600160a01b0390911690634f22429f90613a5d90611192565b60a085015160808901516040808b01515f90815260058a0160205281902060010154905160e086901b6001600160e01b031916815260048101949094526001600160401b0392831660248501529082166044840152818616606484015216608482015260a401602060405180830381865afa158015613ade573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190613b029190615380565b95945050505050565b5f808251602e14613b4157825160405163cc92daa160e01b815263ffffffff9091166004820152602e602482015260440161063c565b5f805b6002811015613b9057613b5881600161591b565b613b63906008615715565b61ffff16858281518110613b7957613b7961540b565b016020015160f81c901b9190911790600101613b44565b5061ffff811615613bba5760405163407b587360e01b815261ffff8216600482015260240161063c565b5f805b6004811015613c1557613bd181600361591b565b613bdc906008615715565b63ffffffff1686613bee8360026156e0565b81518110613bfe57613bfe61540b565b016020015160f81c901b9190911790600101613bbd565b5063ffffffff811615613c3b57604051635b60892f60e01b815260040160405180910390fd5b5f805b6020811015613c9057613c5281601f61591b565b613c5d906008615715565b87613c698360066156e0565b81518110613c7957613c7961540b565b016020015160f81c901b9190911790600101613c3e565b505f805b6008811015613cef57613ca881600761591b565b613cb3906008615715565b6001600160401b031688613cc88360266156e0565b81518110613cd857613cd861540b565b016020015160f81c901b9190911790600101613c94565b5090969095509350505050565b613d04614fac565b5f8281525f80516020615d3e8339815191526020526040808220815160e0810190925280545f80516020615cde83398151915293929190829060ff166005811115613d5157613d516151ce565b6005811115613d6257613d626151ce565b8152602001600182018054613d769061532e565b80601f0160208091040260200160405190810160405280929190818152602001828054613da29061532e565b8015613ded5780601f10613dc457610100808354040283529160200191613ded565b820191905f5260205f20905b815481529060010190602001808311613dd057829003601f168201915b50505091835250506002828101546001600160401b038082166020850152600160401b820481166040850152600160801b820481166060850152600160c01b9091048116608084015260039093015490921660a09091015290915081516005811115613e5b57613e5b6151ce565b14613e8e575f8481526007830160205260409081902054905163170cc93360e21b815261063c9160ff1690600401615360565b60038152426001600160401b031660c08201525f84815260078301602052604090208151815483929190829060ff19166001836005811115613ed257613ed26151ce565b021790555060208201516001820190613eeb9082615611565b5060408201516002820180546060850151608086015160a08701516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909201516003909101805467ffffffffffffffff1916919092161790555f613f898582613817565b6080840151604080516001600160401b03909216825242602083015291935083925087917f13d58394cf269d48bcf927959a29a5ffee7c9924dafff8927ecdf3c48ffa7c67910160405180910390a3509392505050565b804710156140035760405163cd78605960e01b815230600482015260240161063c565b5f826001600160a01b0316826040515f6040518083038185875af1925050503d805f811461404c576040519150601f19603f3d011682016040523d82523d5f602084013e614051565b606091505b505090508061064557604051630a12f52160e11b815260040160405180910390fd5b7fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb09545f9060ff166140b757604051637fab81e560e01b815260040160405180910390fd5b5f80516020615cde833981519152426140d66060860160408701615139565b6001600160401b031611158061411057506140f46202a300426156e0565b6141046060860160408701615139565b6001600160401b031610155b1561414a576141256060850160408601615139565b604051635879da1360e11b81526001600160401b03909116600482015260240161063c565b61415f61415a6060860186615a04565b614942565b61416f61415a6080860186615a04565b603061417e602086018661592e565b9050146141b057614192602085018561592e565b6040516326475b2f60e11b815261063c925060040190815260200190565b6141ba848061592e565b90505f036141e7576141cc848061592e565b604051633e08a12560e11b815260040161063c929190615a18565b5f600882016141f6868061592e565b6040516142049291906157ef565b9081526020016040518091039020541461423d57614222848061592e565b60405163a41f772f60e01b815260040161063c929190615a18565b614247835f6146aa565b6040805160e08101909152815481525f908190614353906020810161426c898061592e565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152505050908252506020908101906142b4908a018a61592e565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152505050908252506020016142fd60608a0160408b01615139565b6001600160401b0316815260200161431860608a018a615a04565b61432190615a46565b815260200161433360808a018a615a04565b61433c90615a46565b8152602001876001600160401b0316815250614aab565b5f828152600686016020526040902091935091506143718282615611565b508160088401614381888061592e565b60405161438f9291906157ef565b9081526040519081900360200181209190915563ee5b48eb60e01b81525f906005600160991b019063ee5b48eb906143cb90859060040161536e565b6020604051808303815f875af11580156143e7573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061440b9190615380565b6040805160e08101909152909150806001815260200161442b898061592e565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201829052509385525050506001600160401b0389166020808401829052604080850184905260608501929092526080840183905260a0909301829052868252600788019092522081518154829060ff191660018360058111156144ba576144ba6151ce565b0217905550602082015160018201906144d39082615611565b5060408201516002820180546060850151608086015160a08701516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909201516003909101805467ffffffffffffffff19169190921617905580614571888061592e565b60405161457f9291906157ef565b6040518091039020847fb77297e3befc691bfc864a81e241f83e2ef722b6e7becaa2ecec250c6d52b430898b60400160208101906145bd9190615139565b604080516001600160401b0393841681529290911660208301520160405180910390a4509095945050505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661463457604051631afcd79f60e31b815260040160405180910390fd5b565b61463e6145eb565b61464781614c98565b61464f614cb1565b610f246060820135608083013561466c60c0850160a08601615139565b61467c60e0860160c08701615b0d565b61468d610100870160e08801615b26565b6101008701356146a561014089016101208a016153ab565b614cc1565b5f80516020615cde8339815191525f6001600160401b0380841690851611156146de576146d7838561581e565b90506146eb565b6146e8848461581e565b90505b6040805160808101825260028401548082526003850154602083015260048501549282019290925260058401546001600160401b031660608201524291158061474d575060018401548151614749916001600160401b0316906156e0565b8210155b15614773576001600160401b038316606082015281815260408101516020820152614792565b828160600181815161478591906157fe565b6001600160401b03169052505b60608101516147a29060646159d9565b602082015160018601546001600160401b0392909216916147cd9190600160401b900460ff16615715565b10156147fd57606081015160405163dfae880160e01b81526001600160401b03909116600482015260240161063c565b856001600160401b03168160400181815161481891906156e0565b9052506040810180516001600160401b038716919061483890839061591b565b9052506001840154604082015160649161485d91600160401b90910460ff1690615715565b1015614884578060400151604051635943317f60e01b815260040161063c91815260200190565b805160028501556020810151600385015560408101516004850155606001516005909301805467ffffffffffffffff19166001600160401b039094169390931790925550505050565b5f8181525f80516020615d3e8339815191526020526040812060020180545f80516020615cde833981519152919060089061491790600160401b90046001600160401b0316615b46565b91906101000a8154816001600160401b0302191690836001600160401b031602179055915050919050565b61494f60208201826150e2565b63ffffffff1615801561496f575061496a60208201826153c6565b151590505b156149b65761498160208201826150e2565b61498e60208301836153c6565b60405163c08a0f1d60e01b815263ffffffff909316600484015260248301525060440161063c565b6149c360208201826153c6565b90506149d260208301836150e2565b63ffffffff1611156149eb5761498160208201826150e2565b60015b6149fb60208301836153c6565b90508110156114a757614a1160208301836153c6565b614a1c60018461591b565b818110614a2b57614a2b61540b565b9050602002016020810190614a4091906153ab565b6001600160a01b0316614a5660208401846153c6565b83818110614a6657614a6661540b565b9050602002016020810190614a7b91906153ab565b6001600160a01b03161015614aa357604051630dbc8d5f60e31b815260040160405180910390fd5b6001016149ee565b5f6060826040015151603014614ad45760405163180ffa0d60e01b815260040160405180910390fd5b82516020808501518051604080880151606089015160808a01518051908701515193515f98614b15988a986001989297929690959094909390929101615b61565b60405160208183030381529060405290505f5b84608001516020015151811015614b8757818560800151602001518281518110614b5457614b5461540b565b6020026020010151604051602001614b6d929190615c1b565b60408051601f198184030181529190529150600101614b28565b5060a0840151805160209182015151604051614ba7938593929101615c51565b60405160208183030381529060405290505f5b8460a001516020015151811015614c1957818560a00151602001518281518110614be657614be661540b565b6020026020010151604051602001614bff929190615c1b565b60408051601f198184030181529190529150600101614bba565b5060c0840151604051614c30918391602001615c8c565b6040516020818303038152906040529050600281604051614c5191906155bc565b602060405180830381855afa158015614c6c573d5f803e3d5ffd5b5050506040513d601f19601f82011682018060405250810190614c8f9190615380565b94909350915050565b614ca06145eb565b614ca8614e7d565b610f2481614e85565b614cb96145eb565b614634614f6d565b614cc96145eb565b5f80516020615cbe83398151915261ffff85161580614ced575061271061ffff8616115b15614d1157604051635f12e6c360e11b815261ffff8616600482015260240161063c565b86881115614d355760405163222d164360e21b81526004810189905260240161063c565b60ff84161580614d485750600a60ff8516115b15614d6b5760405163170db35960e31b815260ff8516600482015260240161063c565b7fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb01546001600160401b03166001600160401b0316866001600160401b03161015614dd2576040516202a06d60e11b81526001600160401b038716600482015260240161063c565b825f03614df25760405163a733007160e01b815260040160405180910390fd5b96875560018701959095556002860180546001600160401b039590951669ffffffffffffffffffff1990951694909417600160401b61ffff94909416939093029290921767ffffffffffffffff60501b191660ff91909116600160501b02179091556003830155600490910180546001600160a01b0319166001600160a01b03909216919091179055565b6146346145eb565b614e8d6145eb565b80355f80516020615cde8339815191529081556014614eb26060840160408501615b26565b60ff161180614ed15750614ecc6060830160408401615b26565b60ff16155b15614f0557614ee66060830160408401615b26565b604051634a59bbff60e11b815260ff909116600482015260240161063c565b614f156060830160408401615b26565b60018201805460ff92909216600160401b0260ff60401b19909216919091179055614f466040830160208401615139565b600191909101805467ffffffffffffffff19166001600160401b0390921691909117905550565b612cea6145eb565b508054614f819061532e565b5f825580601f10614f90575050565b601f0160209004905f5260205f2090810190610f249190614fe9565b6040805160e08101909152805f81526060602082018190525f604083018190529082018190526080820181905260a0820181905260c09091015290565b5b80821115614ffd575f8155600101614fea565b5090565b8015158114610f24575f80fd5b803563ffffffff81168114611a97575f80fd5b5f805f60608486031215615033575f80fd5b83359250602084013561504581615001565b91506150536040850161500e565b90509250925092565b5f6020828403121561506c575f80fd5b5035919050565b5f8060408385031215615084575f80fd5b82356001600160401b03811115615099575f80fd5b8301608081860312156150aa575f80fd5b91506150b86020840161500e565b90509250929050565b5f80604083850312156150d2575f80fd5b823591506150b86020840161500e565b5f602082840312156150f2575f80fd5b6116618261500e565b5f806040838503121561510c575f80fd5b6151158361500e565b946020939093013593505050565b80356001600160401b0381168114611a97575f80fd5b5f60208284031215615149575f80fd5b61166182615123565b803561ffff81168114611a97575f80fd5b5f805f60608486031215615175575f80fd5b83356001600160401b0381111561518a575f80fd5b840160a0818703121561519b575f80fd5b92506151a960208501615152565b915061505360408501615123565b5f61014082840312156151c8575f80fd5b50919050565b634e487b7160e01b5f52602160045260245ffd5b600681106151f2576151f26151ce565b9052565b5f5b838110156152105781810151838201526020016151f8565b50505f910152565b5f815180845261522f8160208601602086016151f6565b601f01601f19169290920160200192915050565b602081526152556020820183516151e2565b5f602083015160e06040840152615270610100840182615218565b905060408401516001600160401b0380821660608601528060608701511660808601528060808701511660a08601528060a08701511660c08601528060c08701511660e086015250508091505092915050565b5f80602083850312156152d4575f80fd5b82356001600160401b03808211156152ea575f80fd5b818501915085601f8301126152fd575f80fd5b81358181111561530b575f80fd5b86602082850101111561531c575f80fd5b60209290920196919550909350505050565b600181811c9082168061534257607f821691505b6020821081036151c857634e487b7160e01b5f52602260045260245ffd5b60208101610e5582846151e2565b602081525f6116616020830184615218565b5f60208284031215615390575f80fd5b5051919050565b6001600160a01b0381168114610f24575f80fd5b5f602082840312156153bb575f80fd5b813561166181615397565b5f808335601e198436030181126153db575f80fd5b8301803591506001600160401b038211156153f4575f80fd5b6020019150600581901b3603821315613967575f80fd5b634e487b7160e01b5f52603260045260245ffd5b5f8235605e19833603018112615433575f80fd5b9190910192915050565b634e487b7160e01b5f52604160045260245ffd5b604051606081016001600160401b03811182821017156154735761547361543d565b60405290565b604080519081016001600160401b03811182821017156154735761547361543d565b604051601f8201601f191681016001600160401b03811182821017156154c3576154c361543d565b604052919050565b5f6001600160401b038211156154e3576154e361543d565b50601f01601f191660200190565b5f82601f830112615500575f80fd5b813561551361550e826154cb565b61549b565b818152846020838601011115615527575f80fd5b816020850160208301375f918101602001919091529392505050565b5f60608236031215615553575f80fd5b61555b615451565b82356001600160401b0380821115615571575f80fd5b61557d368387016154f1565b83526020850135915080821115615592575f80fd5b5061559f368286016154f1565b6020830152506155b160408401615123565b604082015292915050565b5f82516154338184602087016151f6565b601f82111561064557805f5260205f20601f840160051c810160208510156155f25750805b601f840160051c820191505b8181101561184a575f81556001016155fe565b81516001600160401b0381111561562a5761562a61543d565b61563e81615638845461532e565b846155cd565b602080601f831160018114615671575f841561565a5750858301515b5f19600386901b1c1916600185901b178555611056565b5f85815260208120601f198616915b8281101561569f57888601518255948401946001909101908401615680565b50858210156156bc57878501515f19600388901b60f8161c191681555b5050505050600190811b01905550565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610e5557610e556156cc565b5f63ffffffff80831681810361570b5761570b6156cc565b6001019392505050565b8082028115828204841417610e5557610e556156cc565b5f8261574657634e487b7160e01b5f52601260045260245ffd5b500490565b602081016004831061575f5761575f6151ce565b91905290565b5f60208083525f84546157778161532e565b806020870152604060018084165f811461579857600181146157b4576157e1565b60ff19851660408a0152604084151560051b8a010195506157e1565b895f5260205f205f5b858110156157d85781548b82018601529083019088016157bd565b8a016040019650505b509398975050505050505050565b818382375f9101908152919050565b6001600160401b038181168382160190808211156120f5576120f56156cc565b6001600160401b038281168282160390808211156120f5576120f56156cc565b8051611a9781615001565b5f806040838503121561585a575f80fd5b82516001600160401b0380821115615870575f80fd5b9084019060608287031215615883575f80fd5b61588b615451565b8251815260208084015161589e81615397565b828201526040840151838111156158b3575f80fd5b80850194505087601f8501126158c7575f80fd5b835192506158d761550e846154cb565b83815288828587010111156158ea575f80fd5b6158f9848383018488016151f6565b8060408401525081955061590e81880161583e565b9450505050509250929050565b81810381811115610e5557610e556156cc565b5f808335601e19843603018112615943575f80fd5b8301803591506001600160401b0382111561595c575f80fd5b602001915036819003821315613967575f80fd5b5f8851615981818460208d016151f6565b60e089901b6001600160e01b031916908301908152868860048301378681019050600481015f8152858782375060c09390931b6001600160c01b0319166004939094019283019390935250600c019695505050505050565b6001600160401b038181168382160280821691908281146159fc576159fc6156cc565b505092915050565b5f8235603e19833603018112615433575f80fd5b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b5f60408236031215615a56575f80fd5b615a5e615479565b615a678361500e565b81526020808401356001600160401b0380821115615a83575f80fd5b9085019036601f830112615a95575f80fd5b813581811115615aa757615aa761543d565b8060051b9150615ab884830161549b565b8181529183018401918481019036841115615ad1575f80fd5b938501935b83851015615afb5784359250615aeb83615397565b8282529385019390850190615ad6565b94860194909452509295945050505050565b5f60208284031215615b1d575f80fd5b61166182615152565b5f60208284031215615b36575f80fd5b813560ff81168114611661575f80fd5b5f6001600160401b0380831681810361570b5761570b6156cc565b61ffff60f01b8a60f01b1681525f63ffffffff60e01b808b60e01b166002840152896006840152808960e01b166026840152508651615ba781602a850160208b016151f6565b865190830190615bbe81602a840160208b016151f6565b60c087901b6001600160c01b031916602a9290910191820152615bf0603282018660e01b6001600160e01b0319169052565b615c09603682018560e01b6001600160e01b0319169052565b603a019b9a5050505050505050505050565b5f8351615c2c8184602088016151f6565b60609390931b6bffffffffffffffffffffffff19169190920190815260140192915050565b5f8451615c628184602089016151f6565b6001600160e01b031960e095861b8116919093019081529290931b16600482015260080192915050565b5f8351615c9d8184602088016151f6565b60c09390931b6001600160c01b031916919092019081526008019291505056fe4317713f7ecbdddd4bc99e95d903adedaa883b2e7c2551610bd13e2c7e473d00e92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb004317713f7ecbdddd4bc99e95d903adedaa883b2e7c2551610bd13e2c7e473d069b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00e92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb07a164736f6c6343000819000a \ No newline at end of file diff --git a/pkg/validatormanager/deployed_poa_validator_manager_bytecode.txt b/pkg/validatormanager/deployed_poa_validator_manager_bytecode.txt new file mode 100644 index 000000000..99a866e90 --- /dev/null +++ b/pkg/validatormanager/deployed_poa_validator_manager_bytecode.txt @@ -0,0 +1 @@ +0x608060405234801561000f575f80fd5b5060043610610132575f3560e01c80639ba96b86116100b4578063c974d1b611610079578063c974d1b6146102a7578063d588c18f146102af578063d5f20ff6146102c2578063df93d8de146102e2578063f2fde38b146102ec578063fd7ac5e7146102ff575f80fd5b80639ba96b861461024c578063a3a65e481461025f578063b771b3bc14610272578063bc5fbfec14610280578063bee0a03f14610294575f80fd5b8063715018a6116100fa578063715018a6146101be578063732214f8146101c65780638280a25a146101db5780638da5cb5b146101f557806397fb70d414610239575f80fd5b80630322ed981461013657806320d91b7a1461014b578063467ef06f1461015e57806360305d621461017157806366435abf14610193575b5f80fd5b610149610144366004612b01565b610312565b005b610149610159366004612b30565b610529565b61014961016c366004612b7e565b610a15565b610179601481565b60405163ffffffff90911681526020015b60405180910390f35b6101a66101a1366004612b01565b610a23565b6040516001600160401b03909116815260200161018a565b610149610a37565b6101cd5f81565b60405190815260200161018a565b6101e3603081565b60405160ff909116815260200161018a565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b03165b6040516001600160a01b03909116815260200161018a565b610149610247366004612b01565b610a4a565b6101cd61025a366004612bad565b610a5f565b61014961026d366004612b7e565b610a7b565b6102216005600160991b0181565b6101cd5f8051602061370d83398151915281565b6101496102a2366004612b01565b610c04565b6101e3601481565b6101496102bd366004612c06565b610d41565b6102d56102d0366004612b01565b610e4f565b60405161018a9190612cc3565b6101a66202a30081565b6101496102fa366004612d43565b610f9e565b6101cd61030d366004612d65565b610fdb565b5f8181525f8051602061372d8339815191526020526040808220815160e0810190925280545f8051602061370d83398151915293929190829060ff16600581111561035f5761035f612c42565b600581111561037057610370612c42565b815260200160018201805461038490612dd0565b80601f01602080910402602001604051908101604052809291908181526020018280546103b090612dd0565b80156103fb5780601f106103d2576101008083540402835291602001916103fb565b820191905f5260205f20905b8154815290600101906020018083116103de57829003601f168201915b505050918352505060028201546001600160401b038082166020840152600160401b820481166040840152600160801b820481166060840152600160c01b909104811660808301526003928301541660a0909101529091508151600581111561046657610466612c42565b146104a2575f8381526007830160205260409081902054905163170cc93360e21b81526104999160ff1690600401612e08565b60405180910390fd5b6005600160991b016001600160a01b031663ee5b48eb6104c78584606001515f611036565b6040518263ffffffff1660e01b81526004016104e39190612e16565b6020604051808303815f875af11580156104ff573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906105239190612e28565b50505050565b7fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb09545f8051602061370d8339815191529060ff161561057b57604051637fab81e560e01b815260040160405180910390fd5b6005600160991b016001600160a01b0316634213cf786040518163ffffffff1660e01b8152600401602060405180830381865afa1580156105be573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906105e29190612e28565b83602001351461060b576040516372b0a7e760e11b815260208401356004820152602401610499565b3061061c6060850160408601612d43565b6001600160a01b03161461065f5761063a6060840160408501612d43565b604051632f88120d60e21b81526001600160a01b039091166004820152602401610499565b5f61066d6060850185612e3f565b905090505f805b828163ffffffff161015610955575f6106906060880188612e3f565b8363ffffffff168181106106a6576106a6612e84565b90506020028101906106b89190612e98565b6106c190612fbc565b80516040519192505f9160088801916106d991613035565b9081526020016040518091039020541461070957805160405163a41f772f60e01b81526104999190600401612e16565b5f6002885f01358460405160200161073892919091825260e01b6001600160e01b031916602082015260240190565b60408051601f198184030181529082905261075291613035565b602060405180830381855afa15801561076d573d5f803e3d5ffd5b5050506040513d601f19601f820116820180604052508101906107909190612e28565b90508086600801835f01516040516107a89190613035565b90815260408051602092819003830181209390935560e0830181526002835284518284015284810180516001600160401b03908116858401525f60608601819052915181166080860152421660a085015260c0840181905284815260078a01909252902081518154829060ff1916600183600581111561082a5761082a612c42565b0217905550602082015160018201906108439082613091565b506040828101516002830180546060860151608087015160a08801516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909301516003909201805467ffffffffffffffff1916928416929092179091558301516108e8911685613164565b82516040519195506108f991613035565b60408051918290038220908401516001600160401b031682529082907f9d47fef9da077661546e646d61830bfcbda90506c2e5eed38195e82c4eb1cbdf9060200160405180910390a350508061094e90613177565b9050610674565b50600483018190555f61097361096a86611085565b6040015161119b565b90505f61097f87611328565b90505f6002826040516109929190613035565b602060405180830381855afa1580156109ad573d5f803e3d5ffd5b5050506040513d601f19601f820116820180604052508101906109d09190612e28565b90508281146109fc57604051631872fc8d60e01b81526004810182905260248101849052604401610499565b5050506009909201805460ff1916600117905550505050565b610a1e81611561565b505050565b5f610a2d82610e4f565b6080015192915050565b610a3f61189f565b610a485f6118fa565b565b610a5261189f565b610a5b8161196a565b5050565b5f610a6861189f565b610a728383611c4e565b90505b92915050565b5f8051602061370d8339815191525f80610aa0610a9785611085565b604001516121a1565b9150915080610ac657604051632d07135360e01b81528115156004820152602401610499565b5f82815260068401602052604090208054610ae090612dd0565b90505f03610b045760405163089938b360e11b815260048101839052602401610499565b60015f83815260078501602052604090205460ff166005811115610b2a57610b2a612c42565b14610b5d575f8281526007840160205260409081902054905163170cc93360e21b81526104999160ff1690600401612e08565b5f8281526006840160205260408120610b7591612a75565b5f828152600784016020908152604091829020805460ff1916600290811782550180546001600160401b0342818116600160c01b026001600160c01b0390931692909217928390558451600160801b9093041682529181019190915283917ff8fd1c90fb9cfa2ca2358fdf5806b086ad43315d92b221c929efc7f105ce7568910160405180910390a250505050565b5f8181527fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb066020526040902080545f8051602061370d8339815191529190610c4b90612dd0565b90505f03610c6f5760405163089938b360e11b815260048101839052602401610499565b60015f83815260078301602052604090205460ff166005811115610c9557610c95612c42565b14610cc8575f8281526007820160205260409081902054905163170cc93360e21b81526104999160ff1690600401612e08565b5f82815260068201602052604090819020905163ee5b48eb60e01b81526005600160991b019163ee5b48eb91610d019190600401613199565b6020604051808303815f875af1158015610d1d573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a1e9190612e28565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f81158015610d855750825b90505f826001600160401b03166001148015610da05750303b155b905081158015610dae575080155b15610dcc5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff191660011785558315610df657845460ff60401b1916600160401b1785555b610e00878761235d565b8315610e4657845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b610e57612aac565b5f8281525f8051602061372d833981519152602052604090819020815160e0810190925280545f8051602061370d833981519152929190829060ff166005811115610ea457610ea4612c42565b6005811115610eb557610eb5612c42565b8152602001600182018054610ec990612dd0565b80601f0160208091040260200160405190810160405280929190818152602001828054610ef590612dd0565b8015610f405780601f10610f1757610100808354040283529160200191610f40565b820191905f5260205f20905b815481529060010190602001808311610f2357829003601f168201915b505050918352505060028201546001600160401b038082166020840152600160401b820481166040840152600160801b820481166060840152600160c01b9091048116608083015260039092015490911660a0909101529392505050565b610fa661189f565b6001600160a01b038116610fcf57604051631e4fbdf760e01b81525f6004820152602401610499565b610fd8816118fa565b50565b6040515f905f8051602061370d833981519152907fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb089061101e9086908690613223565b90815260200160405180910390205491505092915050565b604080515f6020820152600360e01b602282015260268101949094526001600160c01b031960c093841b811660468601529190921b16604e830152805180830360360181526056909201905290565b60408051606080820183525f8083526020830152918101919091526040516306f8253560e41b815263ffffffff831660048201525f9081906005600160991b0190636f825350906024015f60405180830381865afa1580156110e9573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526111109190810190613241565b915091508061113257604051636b2f19e960e01b815260040160405180910390fd5b815115611158578151604051636ba589a560e01b81526004810191909152602401610499565b60208201516001600160a01b031615611194576020820151604051624de75d60e31b81526001600160a01b039091166004820152602401610499565b5092915050565b5f81516026146111d057815160405163cc92daa160e01b815263ffffffff909116600482015260266024820152604401610499565b5f805b600281101561121f576111e7816001613313565b6111f2906008613326565b61ffff1684828151811061120857611208612e84565b016020015160f81c901b91909117906001016111d3565b5061ffff8116156112495760405163407b587360e01b815261ffff82166004820152602401610499565b5f805b60048110156112a457611260816003613313565b61126b906008613326565b63ffffffff168561127d836002613164565b8151811061128d5761128d612e84565b016020015160f81c901b919091179060010161124c565b5063ffffffff8116156112ca57604051635b60892f60e01b815260040160405180910390fd5b5f805b602081101561131f576112e181601f613313565b6112ec906008613326565b866112f8836006613164565b8151811061130857611308612e84565b016020015160f81c901b91909117906001016112cd565b50949350505050565b60605f8083356020850135601461134487870160408901612d43565b6113516060890189612e3f565b60405160f09790971b6001600160f01b0319166020880152602287019590955250604285019290925260e090811b6001600160e01b0319908116606286015260609290921b6bffffffffffffffffffffffff191660668501529190911b16607a820152607e0160405160208183030381529060405290505f5b6113d76060850185612e3f565b9050811015611194576113ed6060850185612e3f565b828181106113fd576113fd612e84565b905060200281019061140f9190612e98565b61141d90602081019061333d565b905060301461143f5760405163180ffa0d60e01b815260040160405180910390fd5b8161144d6060860186612e3f565b8381811061145d5761145d612e84565b905060200281019061146f9190612e98565b611479908061333d565b90506114886060870187612e3f565b8481811061149857611498612e84565b90506020028101906114aa9190612e98565b6114b4908061333d565b6114c16060890189612e3f565b868181106114d1576114d1612e84565b90506020028101906114e39190612e98565b6114f190602081019061333d565b6114fe60608b018b612e3f565b8881811061150e5761150e612e84565b90506020028101906115209190612e98565b61153190606081019060400161337f565b6040516020016115479796959493929190613398565b60408051601f1981840301815291905291506001016113ca565b5f61156a612aac565b5f8051602061370d8339815191525f80611586610a9787611085565b9150915080156115ad57604051632d07135360e01b81528115156004820152602401610499565b5f828152600784016020526040808220815160e081019092528054829060ff1660058111156115de576115de612c42565b60058111156115ef576115ef612c42565b815260200160018201805461160390612dd0565b80601f016020809104026020016040519081016040528092919081815260200182805461162f90612dd0565b801561167a5780601f106116515761010080835404028352916020019161167a565b820191905f5260205f20905b81548152906001019060200180831161165d57829003601f168201915b505050918352505060028201546001600160401b038082166020840152600160401b820481166040840152600160801b820481166060840152600160c01b909104811660808301526003928301541660a090910152909150815160058111156116e5576116e5612c42565b14158015611706575060018151600581111561170357611703612c42565b14155b1561172757805160405163170cc93360e21b81526104999190600401612e08565b60038151600581111561173c5761173c612c42565b0361174a576004815261174f565b600581525b8360080181602001516040516117659190613035565b90815260408051602092819003830190205f908190558581526007870190925290208151815483929190829060ff191660018360058111156117a9576117a9612c42565b0217905550602082015160018201906117c29082613091565b5060408201516002820180546060850151608086015160a08701516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909201516003909101805467ffffffffffffffff1916919092161790558051600581111561186857611868612c42565b60405184907f1c08e59656f1a18dc2da76826cdc52805c43e897a17c50faefb8ab3c1526cc16905f90a39196919550909350505050565b336118d17f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b031614610a485760405163118cdaa760e01b8152336004820152602401610499565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b611972612aac565b5f8281525f8051602061372d8339815191526020526040808220815160e0810190925280545f8051602061370d83398151915293929190829060ff1660058111156119bf576119bf612c42565b60058111156119d0576119d0612c42565b81526020016001820180546119e490612dd0565b80601f0160208091040260200160405190810160405280929190818152602001828054611a1090612dd0565b8015611a5b5780601f10611a3257610100808354040283529160200191611a5b565b820191905f5260205f20905b815481529060010190602001808311611a3e57829003601f168201915b50505091835250506002828101546001600160401b038082166020850152600160401b820481166040850152600160801b820481166060850152600160c01b9091048116608084015260039093015490921660a09091015290915081516005811115611ac957611ac9612c42565b14611afc575f8481526007830160205260409081902054905163170cc93360e21b81526104999160ff1690600401612e08565b60038152426001600160401b031660c08201525f84815260078301602052604090208151815483929190829060ff19166001836005811115611b4057611b40612c42565b021790555060208201516001820190611b599082613091565b5060408201516002820180546060850151608086015160a08701516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909201516003909101805467ffffffffffffffff1916919092161790555f611bf78582612377565b6080840151604080516001600160401b03909216825242602083015291935083925087917f13d58394cf269d48bcf927959a29a5ffee7c9924dafff8927ecdf3c48ffa7c67910160405180910390a3509392505050565b7fe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb09545f9060ff16611c9257604051637fab81e560e01b815260040160405180910390fd5b5f8051602061370d83398151915242611cb1606086016040870161337f565b6001600160401b0316111580611ceb5750611ccf6202a30042613164565b611cdf606086016040870161337f565b6001600160401b031610155b15611d2557611d00606085016040860161337f565b604051635879da1360e11b81526001600160401b039091166004820152602401610499565b6030611d34602086018661333d565b905014611d6657611d48602085018561333d565b6040516326475b2f60e11b8152610499925060040190815260200190565b611d70848061333d565b90505f03611d9d57611d82848061333d565b604051633e08a12560e11b8152600401610499929190613401565b5f60088201611dac868061333d565b604051611dba929190613223565b90815260200160405180910390205414611df357611dd8848061333d565b60405163a41f772f60e01b8152600401610499929190613401565b611dfd835f6124ce565b6040805160e08101909152815481525f908190611f099060208101611e22898061333d565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250505090825250602090810190611e6a908a018a61333d565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250505090825250602001611eb360608a0160408b0161337f565b6001600160401b03168152602001611ece60608a018a61342f565b611ed790613443565b8152602001611ee960808a018a61342f565b611ef290613443565b8152602001876001600160401b03168152506126a8565b5f82815260068601602052604090209193509150611f278282613091565b508160088401611f37888061333d565b604051611f45929190613223565b9081526040519081900360200181209190915563ee5b48eb60e01b81525f906005600160991b019063ee5b48eb90611f81908590600401612e16565b6020604051808303815f875af1158015611f9d573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611fc19190612e28565b6040805160e081019091529091508060018152602001611fe1898061333d565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201829052509385525050506001600160401b0389166020808401829052604080850184905260608501929092526080840183905260a0909301829052868252600788019092522081518154829060ff1916600183600581111561207057612070612c42565b0217905550602082015160018201906120899082613091565b5060408201516002820180546060850151608086015160a08701516001600160401b039586166001600160801b031990941693909317600160401b92861692909202919091176001600160801b0316600160801b918516919091026001600160c01b031617600160c01b9184169190910217905560c0909201516003909101805467ffffffffffffffff19169190921617905580612127888061333d565b604051612135929190613223565b6040518091039020847fb77297e3befc691bfc864a81e241f83e2ef722b6e7becaa2ecec250c6d52b430898b6040016020810190612173919061337f565b604080516001600160401b0393841681529290911660208301520160405180910390a4509095945050505050565b5f8082516027146121d757825160405163cc92daa160e01b815263ffffffff909116600482015260276024820152604401610499565b5f805b6002811015612226576121ee816001613313565b6121f9906008613326565b61ffff1685828151811061220f5761220f612e84565b016020015160f81c901b91909117906001016121da565b5061ffff8116156122505760405163407b587360e01b815261ffff82166004820152602401610499565b5f805b60048110156122ab57612267816003613313565b612272906008613326565b63ffffffff1686612284836002613164565b8151811061229457612294612e84565b016020015160f81c901b9190911790600101612253565b5063ffffffff81166002146122d357604051635b60892f60e01b815260040160405180910390fd5b5f805b6020811015612328576122ea81601f613313565b6122f5906008613326565b87612301836006613164565b8151811061231157612311612e84565b016020015160f81c901b91909117906001016122d6565b505f8660268151811061233d5761233d612e84565b016020015191976001600160f81b03199092161515965090945050505050565b612365612895565b61236e826128de565b610a5b816128f7565b5f8281525f8051602061372d833981519152602052604081206002015481905f8051602061370d83398151915290600160801b90046001600160401b03166123bf85826124ce565b5f6123c987612908565b5f8881526007850160205260408120600201805467ffffffffffffffff60801b1916600160801b6001600160401b038b16021790559091506005600160991b0163ee5b48eb6124198a858b611036565b6040518263ffffffff1660e01b81526004016124359190612e16565b6020604051808303815f875af1158015612451573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906124759190612e28565b604080516001600160401b038a811682526020820184905282519394508516928b927f07de5ff35a674a8005e661f3333c907ca6333462808762d19dc7b3abb1a8c1df928290030190a3909450925050505b9250929050565b5f8051602061370d8339815191525f6001600160401b038084169085161115612502576124fb838561350a565b905061250f565b61250c848461350a565b90505b6040805160808101825260028401548082526003850154602083015260048501549282019290925260058401546001600160401b031660608201524291158061257157506001840154815161256d916001600160401b031690613164565b8210155b15612597576001600160401b0383166060820152818152604081015160208201526125b6565b82816060018181516125a9919061352a565b6001600160401b03169052505b60608101516125c690606461354a565b602082015160018601546001600160401b0392909216916125f19190600160401b900460ff16613326565b101561262157606081015160405163dfae880160e01b81526001600160401b039091166004820152602401610499565b856001600160401b03168160400181815161263c9190613164565b9052506040810180516001600160401b038716919061265c908390613313565b905250805160028501556020810151600385015560408101516004850155606001516005909301805467ffffffffffffffff19166001600160401b039094169390931790925550505050565b5f60608260400151516030146126d15760405163180ffa0d60e01b815260040160405180910390fd5b82516020808501518051604080880151606089015160808a01518051908701515193515f98612712988a986001989297929690959094909390929101613575565b60405160208183030381529060405290505f5b846080015160200151518110156127845781856080015160200151828151811061275157612751612e84565b602002602001015160405160200161276a92919061362f565b60408051601f198184030181529190529150600101612725565b5060a08401518051602091820151516040516127a4938593929101613665565b60405160208183030381529060405290505f5b8460a00151602001515181101561281657818560a001516020015182815181106127e3576127e3612e84565b60200260200101516040516020016127fc92919061362f565b60408051601f1981840301815291905291506001016127b7565b5060c084015160405161282d9183916020016136a0565b604051602081830303815290604052905060028160405161284e9190613035565b602060405180830381855afa158015612869573d5f803e3d5ffd5b5050506040513d601f19601f8201168201806040525081019061288c9190612e28565b94909350915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610a4857604051631afcd79f60e31b815260040160405180910390fd5b6128e6612895565b6128ee61297d565b610fd881612985565b6128ff612895565b610fd881612a6d565b5f8181525f8051602061372d8339815191526020526040812060020180545f8051602061370d833981519152919060089061295290600160401b90046001600160401b03166136d1565b91906101000a8154816001600160401b0302191690836001600160401b031602179055915050919050565b610a48612895565b61298d612895565b80355f8051602061370d83398151915290815560146129b260608401604085016136ec565b60ff1611806129d157506129cc60608301604084016136ec565b60ff16155b15612a05576129e660608301604084016136ec565b604051634a59bbff60e11b815260ff9091166004820152602401610499565b612a1560608301604084016136ec565b60018201805460ff92909216600160401b0260ff60401b19909216919091179055612a46604083016020840161337f565b600191909101805467ffffffffffffffff19166001600160401b0390921691909117905550565b610fa6612895565b508054612a8190612dd0565b5f825580601f10612a90575050565b601f0160209004905f5260205f2090810190610fd89190612ae9565b6040805160e08101909152805f81526060602082018190525f604083018190529082018190526080820181905260a0820181905260c09091015290565b5b80821115612afd575f8155600101612aea565b5090565b5f60208284031215612b11575f80fd5b5035919050565b803563ffffffff81168114612b2b575f80fd5b919050565b5f8060408385031215612b41575f80fd5b82356001600160401b03811115612b56575f80fd5b830160808186031215612b67575f80fd5b9150612b7560208401612b18565b90509250929050565b5f60208284031215612b8e575f80fd5b610a7282612b18565b80356001600160401b0381168114612b2b575f80fd5b5f8060408385031215612bbe575f80fd5b82356001600160401b03811115612bd3575f80fd5b830160a08186031215612be4575f80fd5b9150612b7560208401612b97565b6001600160a01b0381168114610fd8575f80fd5b5f808284036080811215612c18575f80fd5b6060811215612c25575f80fd5b508291506060830135612c3781612bf2565b809150509250929050565b634e487b7160e01b5f52602160045260245ffd5b60068110612c7257634e487b7160e01b5f52602160045260245ffd5b9052565b5f5b83811015612c90578181015183820152602001612c78565b50505f910152565b5f8151808452612caf816020860160208601612c76565b601f01601f19169290920160200192915050565b60208152612cd5602082018351612c56565b5f602083015160e06040840152612cf0610100840182612c98565b905060408401516001600160401b0380821660608601528060608701511660808601528060808701511660a08601528060a08701511660c08601528060c08701511660e086015250508091505092915050565b5f60208284031215612d53575f80fd5b8135612d5e81612bf2565b9392505050565b5f8060208385031215612d76575f80fd5b82356001600160401b0380821115612d8c575f80fd5b818501915085601f830112612d9f575f80fd5b813581811115612dad575f80fd5b866020828501011115612dbe575f80fd5b60209290920196919550909350505050565b600181811c90821680612de457607f821691505b602082108103612e0257634e487b7160e01b5f52602260045260245ffd5b50919050565b60208101610a758284612c56565b602081525f610a726020830184612c98565b5f60208284031215612e38575f80fd5b5051919050565b5f808335601e19843603018112612e54575f80fd5b8301803591506001600160401b03821115612e6d575f80fd5b6020019150600581901b36038213156124c7575f80fd5b634e487b7160e01b5f52603260045260245ffd5b5f8235605e19833603018112612eac575f80fd5b9190910192915050565b634e487b7160e01b5f52604160045260245ffd5b604051606081016001600160401b0381118282101715612eec57612eec612eb6565b60405290565b604080519081016001600160401b0381118282101715612eec57612eec612eb6565b604051601f8201601f191681016001600160401b0381118282101715612f3c57612f3c612eb6565b604052919050565b5f6001600160401b03821115612f5c57612f5c612eb6565b50601f01601f191660200190565b5f82601f830112612f79575f80fd5b8135612f8c612f8782612f44565b612f14565b818152846020838601011115612fa0575f80fd5b816020850160208301375f918101602001919091529392505050565b5f60608236031215612fcc575f80fd5b612fd4612eca565b82356001600160401b0380821115612fea575f80fd5b612ff636838701612f6a565b8352602085013591508082111561300b575f80fd5b5061301836828601612f6a565b60208301525061302a60408401612b97565b604082015292915050565b5f8251612eac818460208701612c76565b601f821115610a1e57805f5260205f20601f840160051c8101602085101561306b5750805b601f840160051c820191505b8181101561308a575f8155600101613077565b5050505050565b81516001600160401b038111156130aa576130aa612eb6565b6130be816130b88454612dd0565b84613046565b602080601f8311600181146130f1575f84156130da5750858301515b5f19600386901b1c1916600185901b178555613148565b5f85815260208120601f198616915b8281101561311f57888601518255948401946001909101908401613100565b508582101561313c57878501515f19600388901b60f8161c191681555b505060018460011b0185555b505050505050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610a7557610a75613150565b5f63ffffffff80831681810361318f5761318f613150565b6001019392505050565b5f60208083525f84546131ab81612dd0565b806020870152604060018084165f81146131cc57600181146131e857613215565b60ff19851660408a0152604084151560051b8a01019550613215565b895f5260205f205f5b8581101561320c5781548b82018601529083019088016131f1565b8a016040019650505b509398975050505050505050565b818382375f9101908152919050565b80518015158114612b2b575f80fd5b5f8060408385031215613252575f80fd5b82516001600160401b0380821115613268575f80fd5b908401906060828703121561327b575f80fd5b613283612eca565b8251815260208084015161329681612bf2565b828201526040840151838111156132ab575f80fd5b80850194505087601f8501126132bf575f80fd5b835192506132cf612f8784612f44565b83815288828587010111156132e2575f80fd5b6132f184838301848801612c76565b80604084015250819550613306818801613232565b9450505050509250929050565b81810381811115610a7557610a75613150565b8082028115828204841417610a7557610a75613150565b5f808335601e19843603018112613352575f80fd5b8301803591506001600160401b0382111561336b575f80fd5b6020019150368190038213156124c7575f80fd5b5f6020828403121561338f575f80fd5b610a7282612b97565b5f88516133a9818460208d01612c76565b60e089901b6001600160e01b031916908301908152868860048301378681019050600481015f8152858782375060c09390931b6001600160c01b0319166004939094019283019390935250600c019695505050505050565b60208152816020820152818360408301375f818301604090810191909152601f909201601f19160101919050565b5f8235603e19833603018112612eac575f80fd5b5f60408236031215613453575f80fd5b61345b612ef2565b61346483612b18565b81526020808401356001600160401b0380821115613480575f80fd5b9085019036601f830112613492575f80fd5b8135818111156134a4576134a4612eb6565b8060051b91506134b5848301612f14565b81815291830184019184810190368411156134ce575f80fd5b938501935b838510156134f857843592506134e883612bf2565b82825293850193908501906134d3565b94860194909452509295945050505050565b6001600160401b0382811682821603908082111561119457611194613150565b6001600160401b0381811683821601908082111561119457611194613150565b6001600160401b0381811683821602808216919082811461356d5761356d613150565b505092915050565b61ffff60f01b8a60f01b1681525f63ffffffff60e01b808b60e01b166002840152896006840152808960e01b1660268401525086516135bb81602a850160208b01612c76565b8651908301906135d281602a840160208b01612c76565b60c087901b6001600160c01b031916602a9290910191820152613604603282018660e01b6001600160e01b0319169052565b61361d603682018560e01b6001600160e01b0319169052565b603a019b9a5050505050505050505050565b5f8351613640818460208801612c76565b60609390931b6bffffffffffffffffffffffff19169190920190815260140192915050565b5f8451613676818460208901612c76565b6001600160e01b031960e095861b8116919093019081529290931b16600482015260080192915050565b5f83516136b1818460208801612c76565b60c09390931b6001600160c01b0319169190920190815260080192915050565b5f6001600160401b0380831681810361318f5761318f613150565b5f602082840312156136fc575f80fd5b813560ff81168114612d5e575f80fdfee92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb00e92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb07a164736f6c6343000819000a diff --git a/pkg/validatormanager/deployed_proxy_admin_bytecode.txt b/pkg/validatormanager/deployed_proxy_admin_bytecode.txt new file mode 100644 index 000000000..0af2ca620 --- /dev/null +++ b/pkg/validatormanager/deployed_proxy_admin_bytecode.txt @@ -0,0 +1 @@ +0x60806040526004361061007b5760003560e01c80639623609d1161004e5780639623609d1461011157806399a88ec414610124578063f2fde38b14610144578063f3b7dead1461016457600080fd5b8063204e1c7a14610080578063715018a6146100bc5780637eff275e146100d35780638da5cb5b146100f3575b600080fd5b34801561008c57600080fd5b506100a061009b366004610499565b610184565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c857600080fd5b506100d1610215565b005b3480156100df57600080fd5b506100d16100ee3660046104bd565b610229565b3480156100ff57600080fd5b506000546001600160a01b03166100a0565b6100d161011f36600461050c565b610291565b34801561013057600080fd5b506100d161013f3660046104bd565b610300565b34801561015057600080fd5b506100d161015f366004610499565b610336565b34801561017057600080fd5b506100a061017f366004610499565b6103b4565b6000806000836001600160a01b03166040516101aa90635c60da1b60e01b815260040190565b600060405180830381855afa9150503d80600081146101e5576040519150601f19603f3d011682016040523d82523d6000602084013e6101ea565b606091505b5091509150816101f957600080fd5b8080602001905181019061020d91906105e2565b949350505050565b61021d6103da565b6102276000610434565b565b6102316103da565b6040516308f2839760e41b81526001600160a01b038281166004830152831690638f283970906024015b600060405180830381600087803b15801561027557600080fd5b505af1158015610289573d6000803e3d6000fd5b505050505050565b6102996103da565b60405163278f794360e11b81526001600160a01b03841690634f1ef2869034906102c990869086906004016105ff565b6000604051808303818588803b1580156102e257600080fd5b505af11580156102f6573d6000803e3d6000fd5b5050505050505050565b6103086103da565b604051631b2ce7f360e11b81526001600160a01b038281166004830152831690633659cfe69060240161025b565b61033e6103da565b6001600160a01b0381166103a85760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084015b60405180910390fd5b6103b181610434565b50565b6000806000836001600160a01b03166040516101aa906303e1469160e61b815260040190565b6000546001600160a01b031633146102275760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161039f565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6001600160a01b03811681146103b157600080fd5b6000602082840312156104ab57600080fd5b81356104b681610484565b9392505050565b600080604083850312156104d057600080fd5b82356104db81610484565b915060208301356104eb81610484565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b60008060006060848603121561052157600080fd5b833561052c81610484565b9250602084013561053c81610484565b9150604084013567ffffffffffffffff8082111561055957600080fd5b818601915086601f83011261056d57600080fd5b81358181111561057f5761057f6104f6565b604051601f8201601f19908116603f011681019083821181831017156105a7576105a76104f6565b816040528281528960208487010111156105c057600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b6000602082840312156105f457600080fd5b81516104b681610484565b60018060a01b03831681526000602060406020840152835180604085015260005b8181101561063c57858101830151858201606001528201610620565b506000606082860101526060601f19601f83011685010192505050939250505056fea264697066735822122019f39983a6fd15f3cffa764efd6fb0234ffe8d71051b3ebddc0b6bd99f87fa9764736f6c63430008190033 \ No newline at end of file diff --git a/pkg/validatormanager/deployed_reward_calculator_bytecode.txt b/pkg/validatormanager/deployed_reward_calculator_bytecode.txt new file mode 100644 index 000000000..cf51ff238 --- /dev/null +++ b/pkg/validatormanager/deployed_reward_calculator_bytecode.txt @@ -0,0 +1 @@ +0x608060405234801561000f575f80fd5b5060043610610055575f3560e01c80634f22429f146100595780635dcc93911461007f578063a9778a7a1461008a578063afba878a146100a6578063bb65b242146100c0575b5f80fd5b61006c6100673660046101a1565b6100ec565b6040519081526020015b60405180910390f35b61006c6301e1338081565b61009361271081565b60405161ffff9091168152602001610076565b6100ae605081565b60405160ff9091168152602001610076565b5f546100d39067ffffffffffffffff1681565b60405167ffffffffffffffff9091168152602001610076565b5f60506100f9868561020f565b6101039190610237565b67ffffffffffffffff16610118836064610237565b67ffffffffffffffff16101561012f57505f61017c565b6127106301e13380610141868661020f565b5f5467ffffffffffffffff9182169161015b91168a610263565b6101659190610263565b61016f9190610280565b6101799190610280565b90505b95945050505050565b803567ffffffffffffffff8116811461019c575f80fd5b919050565b5f805f805f60a086880312156101b5575f80fd5b853594506101c560208701610185565b93506101d360408701610185565b92506101e160608701610185565b91506101ef60808701610185565b90509295509295909350565b634e487b7160e01b5f52601160045260245ffd5b67ffffffffffffffff828116828216039080821115610230576102306101fb565b5092915050565b67ffffffffffffffff81811683821602808216919082811461025b5761025b6101fb565b505092915050565b808202811582820484141761027a5761027a6101fb565b92915050565b5f8261029a57634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c6343000819000a \ No newline at end of file diff --git a/pkg/validatormanager/deployed_transparent_proxy_bytecode.txt b/pkg/validatormanager/deployed_transparent_proxy_bytecode.txt new file mode 100644 index 000000000..b254795ba --- /dev/null +++ b/pkg/validatormanager/deployed_transparent_proxy_bytecode.txt @@ -0,0 +1 @@ +0x60806040523661001357610011610017565b005b6100115b61001f610169565b6001600160a01b0316330361015f5760606001600160e01b0319600035166364d3180d60e11b810161005a5761005361019c565b9150610157565b63587086bd60e11b6001600160e01b031982160161007a576100536101f3565b63070d7c6960e41b6001600160e01b031982160161009a57610053610239565b621eb96f60e61b6001600160e01b03198216016100b95761005361026a565b63a39f25e560e01b6001600160e01b03198216016100d9576100536102aa565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b6101676102be565b565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101a66102ce565b60006101b53660048184610683565b8101906101c291906106c9565b90506101df816040518060200160405280600081525060006102d9565b505060408051602081019091526000815290565b60606000806102053660048184610683565b81019061021291906106fa565b91509150610222828260016102d9565b604051806020016040528060008152509250505090565b60606102436102ce565b60006102523660048184610683565b81019061025f91906106c9565b90506101df81610305565b60606102746102ce565b600061027e610169565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b60606102b46102ce565b600061027e61035c565b6101676102c961035c565b61036b565b341561016757600080fd5b6102e28361038f565b6000825111806102ef5750805b15610300576102fe83836103cf565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f61032e610169565b604080516001600160a01b03928316815291841660208301520160405180910390a1610359816103fb565b50565b60006103666104a4565b905090565b3660008037600080366000845af43d6000803e80801561038a573d6000f35b3d6000fd5b610398816104cc565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b60606103f4838360405180606001604052806027815260200161083060279139610560565b9392505050565b6001600160a01b0381166104605760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161014e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61018d565b6001600160a01b0381163b6105395760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161014e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc610483565b6060600080856001600160a01b03168560405161057d91906107e0565b600060405180830381855af49150503d80600081146105b8576040519150601f19603f3d011682016040523d82523d6000602084013e6105bd565b606091505b50915091506105ce868383876105d8565b9695505050505050565b60608315610647578251600003610640576001600160a01b0385163b6106405760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161014e565b5081610651565b6106518383610659565b949350505050565b8151156106695781518083602001fd5b8060405162461bcd60e51b815260040161014e91906107fc565b6000808585111561069357600080fd5b838611156106a057600080fd5b5050820193919092039150565b80356001600160a01b03811681146106c457600080fd5b919050565b6000602082840312156106db57600080fd5b6103f4826106ad565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561070d57600080fd5b610716836106ad565b9150602083013567ffffffffffffffff8082111561073357600080fd5b818501915085601f83011261074757600080fd5b813581811115610759576107596106e4565b604051601f8201601f19908116603f01168101908382118183101715610781576107816106e4565b8160405282815288602084870101111561079a57600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107d75781810151838201526020016107bf565b50506000910152565b600082516107f28184602087016107bc565b9190910192915050565b602081526000825180602084015261081b8160408501602087016107bc565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220b22984eb1f3348f5b2148862b6f80392e497e3c65d0d2cfbb5e53d737e5a6c6a64736f6c63430008190033 \ No newline at end of file diff --git a/pkg/validatormanager/registration.go b/pkg/validatormanager/registration.go new file mode 100644 index 000000000..b734d0bf8 --- /dev/null +++ b/pkg/validatormanager/registration.go @@ -0,0 +1,555 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package validatormanager + +import ( + _ "embed" + "errors" + "fmt" + "math/big" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/sdk/interchain" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/proto/pb/platformvm" + avagoconstants "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + warp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/interfaces" + subnetEvmWarp "github.com/ava-labs/subnet-evm/precompile/contracts/warp" + "github.com/ethereum/go-ethereum/common" + "google.golang.org/protobuf/proto" +) + +func NativePoSValidatorManagerInitializeValidatorRegistration( + rpcURL string, + managerAddress common.Address, + managerOwnerPrivateKey string, + nodeID ids.NodeID, + blsPublicKey []byte, + expiry uint64, + balanceOwners warpMessage.PChainOwner, + disableOwners warpMessage.PChainOwner, + delegationFeeBips uint16, + minStakeDuration time.Duration, + stakeAmount *big.Int, +) (*types.Transaction, *types.Receipt, error) { + type PChainOwner struct { + Threshold uint32 + Addresses []common.Address + } + + type ValidatorRegistrationInput struct { + NodeID []byte + BlsPublicKey []byte + RegistrationExpiry uint64 + RemainingBalanceOwner PChainOwner + DisableOwner PChainOwner + } + + balanceOwnersAux := PChainOwner{ + Threshold: balanceOwners.Threshold, + Addresses: utils.Map(balanceOwners.Addresses, func(addr ids.ShortID) common.Address { + return common.BytesToAddress(addr[:]) + }), + } + disableOwnersAux := PChainOwner{ + Threshold: disableOwners.Threshold, + Addresses: utils.Map(disableOwners.Addresses, func(addr ids.ShortID) common.Address { + return common.BytesToAddress(addr[:]) + }), + } + validatorRegistrationInput := ValidatorRegistrationInput{ + NodeID: nodeID[:], + BlsPublicKey: blsPublicKey, + RegistrationExpiry: expiry, + RemainingBalanceOwner: balanceOwnersAux, + DisableOwner: disableOwnersAux, + } + + return contract.TxToMethod( + rpcURL, + managerOwnerPrivateKey, + managerAddress, + stakeAmount, + "initialize validator registration with stake", + validatorManagerSDK.ErrorSignatureToError, + "initializeValidatorRegistration((bytes,bytes,uint64,(uint32,[address]),(uint32,[address])),uint16,uint64)", + validatorRegistrationInput, + delegationFeeBips, + uint64(minStakeDuration.Seconds()), + ) +} + +// step 1 of flow for adding a new validator +func PoAValidatorManagerInitializeValidatorRegistration( + rpcURL string, + managerAddress common.Address, + managerOwnerPrivateKey string, + nodeID ids.NodeID, + blsPublicKey []byte, + expiry uint64, + balanceOwners warpMessage.PChainOwner, + disableOwners warpMessage.PChainOwner, + weight uint64, +) (*types.Transaction, *types.Receipt, error) { + type PChainOwner struct { + Threshold uint32 + Addresses []common.Address + } + type ValidatorRegistrationInput struct { + NodeID []byte + BlsPublicKey []byte + RegistrationExpiry uint64 + RemainingBalanceOwner PChainOwner + DisableOwner PChainOwner + } + balanceOwnersAux := PChainOwner{ + Threshold: balanceOwners.Threshold, + Addresses: utils.Map(balanceOwners.Addresses, func(addr ids.ShortID) common.Address { + return common.BytesToAddress(addr[:]) + }), + } + disableOwnersAux := PChainOwner{ + Threshold: disableOwners.Threshold, + Addresses: utils.Map(disableOwners.Addresses, func(addr ids.ShortID) common.Address { + return common.BytesToAddress(addr[:]) + }), + } + validatorRegistrationInput := ValidatorRegistrationInput{ + NodeID: nodeID[:], + BlsPublicKey: blsPublicKey, + RegistrationExpiry: expiry, + RemainingBalanceOwner: balanceOwnersAux, + DisableOwner: disableOwnersAux, + } + return contract.TxToMethod( + rpcURL, + managerOwnerPrivateKey, + managerAddress, + big.NewInt(0), + "initialize validator registration", + validatorManagerSDK.ErrorSignatureToError, + "initializeValidatorRegistration((bytes,bytes,uint64,(uint32,[address]),(uint32,[address])),uint64)", + validatorRegistrationInput, + weight, + ) +} + +func ValidatorManagerGetSubnetValidatorRegistrationMessage( + network models.Network, + aggregatorLogLevel logging.Level, + aggregatorQuorumPercentage uint64, + aggregatorExtraPeerEndpoints []info.Peer, + subnetID ids.ID, + blockchainID ids.ID, + managerAddress common.Address, + nodeID ids.NodeID, + blsPublicKey [48]byte, + expiry uint64, + balanceOwners warpMessage.PChainOwner, + disableOwners warpMessage.PChainOwner, + weight uint64, +) (*warp.Message, ids.ID, error) { + addressedCallPayload, err := warpMessage.NewRegisterL1Validator( + subnetID, + nodeID, + blsPublicKey, + expiry, + balanceOwners, + disableOwners, + weight, + ) + if err != nil { + return nil, ids.Empty, err + } + validationID := addressedCallPayload.ValidationID() + registerSubnetValidatorAddressedCall, err := warpPayload.NewAddressedCall( + managerAddress.Bytes(), + addressedCallPayload.Bytes(), + ) + if err != nil { + return nil, ids.Empty, err + } + registerSubnetValidatorUnsignedMessage, err := warp.NewUnsignedMessage( + network.ID, + blockchainID, + registerSubnetValidatorAddressedCall.Bytes(), + ) + if err != nil { + return nil, ids.Empty, err + } + signatureAggregator, err := interchain.NewSignatureAggregator( + network, + aggregatorLogLevel, + subnetID, + aggregatorQuorumPercentage, + aggregatorExtraPeerEndpoints, + ) + if err != nil { + return nil, ids.Empty, err + } + signedMessage, err := signatureAggregator.Sign(registerSubnetValidatorUnsignedMessage, nil) + return signedMessage, validationID, err +} + +func GetRegisteredValidator( + rpcURL string, + managerAddress common.Address, + nodeID ids.NodeID, +) (ids.ID, error) { + out, err := contract.CallToMethod( + rpcURL, + managerAddress, + "registeredValidators(bytes)->(bytes32)", + nodeID[:], + ) + if err != nil { + return ids.Empty, err + } + validatorID, b := out[0].([32]byte) + if !b { + return ids.Empty, fmt.Errorf("error at registeredValidators call, expected [32]byte, got %T", out[0]) + } + return validatorID, nil +} + +func GetValidatorWeight( + rpcURL string, + managerAddress common.Address, + validatorID ids.ID, +) (uint64, error) { + out, err := contract.CallToMethod( + rpcURL, + managerAddress, + "getWeight(bytes32)->(uint64)", + validatorID, + ) + if err != nil { + return 0, err + } + weight, b := out[0].(uint64) + if !b { + return 0, fmt.Errorf("error at getWeight call, expected uint64, got %T", out[0]) + } + return weight, nil +} + +func ValidatorManagerGetPChainSubnetValidatorRegistrationWarpMessage( + network models.Network, + rpcURL string, + aggregatorLogLevel logging.Level, + aggregatorQuorumPercentage uint64, + aggregatorExtraPeerEndpoints []info.Peer, + subnetID ids.ID, + validationID ids.ID, + registered bool, +) (*warp.Message, error) { + addressedCallPayload, err := warpMessage.NewL1ValidatorRegistration(validationID, registered) + if err != nil { + return nil, err + } + subnetValidatorRegistrationAddressedCall, err := warpPayload.NewAddressedCall( + nil, + addressedCallPayload.Bytes(), + ) + if err != nil { + return nil, err + } + subnetConversionUnsignedMessage, err := warp.NewUnsignedMessage( + network.ID, + avagoconstants.PlatformChainID, + subnetValidatorRegistrationAddressedCall.Bytes(), + ) + if err != nil { + return nil, err + } + signatureAggregator, err := interchain.NewSignatureAggregator( + network, + aggregatorLogLevel, + subnetID, + aggregatorQuorumPercentage, + aggregatorExtraPeerEndpoints, + ) + if err != nil { + return nil, err + } + var justificationBytes []byte + if !registered { + justificationBytes, err = GetRegistrationMessage(rpcURL, validationID, subnetID) + if err != nil { + return nil, err + } + } + return signatureAggregator.Sign(subnetConversionUnsignedMessage, justificationBytes) +} + +// last step of flow for adding a new validator +func ValidatorManagerCompleteValidatorRegistration( + rpcURL string, + managerAddress common.Address, + privateKey string, // not need to be owner atm + subnetValidatorRegistrationSignedMessage *warp.Message, +) (*types.Transaction, *types.Receipt, error) { + return contract.TxToMethodWithWarpMessage( + rpcURL, + privateKey, + managerAddress, + subnetValidatorRegistrationSignedMessage, + big.NewInt(0), + "complete validator registration", + validatorManagerSDK.ErrorSignatureToError, + "completeValidatorRegistration(uint32)", + uint32(0), + ) +} + +func InitValidatorRegistration( + app *application.Avalanche, + network models.Network, + rpcURL string, + chainSpec contract.ChainSpec, + ownerPrivateKey string, + nodeID ids.NodeID, + blsPublicKey []byte, + expiry uint64, + balanceOwners warpMessage.PChainOwner, + disableOwners warpMessage.PChainOwner, + weight uint64, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevelStr string, + initWithPos bool, + delegationFee uint16, + stakeDuration time.Duration, + stakeAmount *big.Int, +) (*warp.Message, ids.ID, error) { + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return nil, ids.Empty, err + } + blockchainID, err := contract.GetBlockchainID( + app, + network, + chainSpec, + ) + if err != nil { + return nil, ids.Empty, err + } + managerAddress := common.HexToAddress(validatorManagerSDK.ProxyContractAddress) + if initWithPos { + ux.Logger.PrintLineSeparator() + ux.Logger.PrintToUser("Initializing a validator registration with PoS validator manager") + ux.Logger.PrintToUser("Using rpcURL: %s", rpcURL) + ux.Logger.PrintToUser("NodeID: %s staking %s for %ds", nodeID.String(), stakeAmount, uint64(stakeDuration.Seconds())) + ux.Logger.PrintLineSeparator() + tx, _, err := NativePoSValidatorManagerInitializeValidatorRegistration( + rpcURL, + managerAddress, + ownerPrivateKey, + nodeID, + blsPublicKey, + expiry, + balanceOwners, + disableOwners, + delegationFee, + stakeDuration, + stakeAmount, + ) + if err != nil { + if !errors.Is(err, validatorManagerSDK.ErrNodeAlreadyRegistered) { + return nil, ids.Empty, evm.TransactionError(tx, err, "failure initializing validator registration") + } + ux.Logger.PrintToUser("the validator registration was already initialized. Proceeding to the next step") + } + } else { + managerAddress = common.HexToAddress(validatorManagerSDK.ProxyContractAddress) + tx, _, err := PoAValidatorManagerInitializeValidatorRegistration( + rpcURL, + managerAddress, + ownerPrivateKey, + nodeID, + blsPublicKey, + expiry, + balanceOwners, + disableOwners, + weight, + ) + if err != nil { + if !errors.Is(err, validatorManagerSDK.ErrNodeAlreadyRegistered) { + return nil, ids.Empty, evm.TransactionError(tx, err, "failure initializing validator registration") + } + ux.Logger.PrintToUser("the validator registration was already initialized. Proceeding to the next step") + } + } + aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) + if err != nil { + aggregatorLogLevel = defaultAggregatorLogLevel + } + if initWithPos { + validationID, err := GetRegisteredValidator(rpcURL, managerAddress, nodeID) + if err != nil { + ux.Logger.PrintToUser("Error getting validation ID") + return nil, ids.Empty, err + } + weight, err = GetValidatorWeight(rpcURL, managerAddress, validationID) + if err != nil { + ux.Logger.PrintToUser("Error getting validator weight") + return nil, ids.Empty, err + } + } + + ux.Logger.PrintToUser(fmt.Sprintf("Validator weight: %d", weight)) + return ValidatorManagerGetSubnetValidatorRegistrationMessage( + network, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + subnetID, + blockchainID, + managerAddress, + nodeID, + [48]byte(blsPublicKey), + expiry, + balanceOwners, + disableOwners, + weight, + ) +} + +func FinishValidatorRegistration( + app *application.Avalanche, + network models.Network, + rpcURL string, + chainSpec contract.ChainSpec, + privateKey string, + validationID ids.ID, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevelStr string, +) error { + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) + if err != nil { + aggregatorLogLevel = defaultAggregatorLogLevel + } + managerAddress := common.HexToAddress(validatorManagerSDK.ProxyContractAddress) + signedMessage, err := ValidatorManagerGetPChainSubnetValidatorRegistrationWarpMessage( + network, + rpcURL, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + subnetID, + validationID, + true, + ) + if err != nil { + return err + } + if err := evm.SetupProposerVM( + rpcURL, + privateKey, + ); err != nil { + return err + } + tx, _, err := ValidatorManagerCompleteValidatorRegistration( + rpcURL, + managerAddress, + privateKey, + signedMessage, + ) + if err != nil { + return evm.TransactionError(tx, err, "failure completing validator registration") + } + return nil +} + +func GetRegistrationMessage( + rpcURL string, + validationID ids.ID, + subnetID ids.ID, +) ([]byte, error) { + const numBootstrapValidatorsToSearch = 100 + for validationIndex := uint32(0); validationIndex < numBootstrapValidatorsToSearch; validationIndex++ { + bootstrapValidationID := subnetID.Append(validationIndex) + if bootstrapValidationID == validationID { + justification := platformvm.L1ValidatorRegistrationJustification{ + Preimage: &platformvm.L1ValidatorRegistrationJustification_ConvertSubnetToL1TxData{ + ConvertSubnetToL1TxData: &platformvm.SubnetIDIndex{ + SubnetId: subnetID[:], + Index: validationIndex, + }, + }, + } + return proto.Marshal(&justification) + } + } + client, err := evm.GetClient(rpcURL) + if err != nil { + return nil, err + } + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + height, err := client.BlockNumber(ctx) + if err != nil { + return nil, err + } + for blockNumber := uint64(0); blockNumber <= height; blockNumber++ { + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + block, err := client.BlockByNumber(ctx, big.NewInt(int64(blockNumber))) + if err != nil { + return nil, err + } + blockHash := block.Hash() + logs, err := client.FilterLogs(ctx, interfaces.FilterQuery{ + BlockHash: &blockHash, + Addresses: []common.Address{subnetEvmWarp.Module.Address}, + }) + if err != nil { + return nil, err + } + for _, txLog := range logs { + msg, err := subnetEvmWarp.UnpackSendWarpEventDataToMessage(txLog.Data) + if err == nil { + payload := msg.Payload + addressedCall, err := warpPayload.ParseAddressedCall(payload) + if err == nil { + reg, err := warpMessage.ParseRegisterL1Validator(addressedCall.Payload) + if err == nil { + if reg.ValidationID() == validationID { + justification := platformvm.L1ValidatorRegistrationJustification{ + Preimage: &platformvm.L1ValidatorRegistrationJustification_RegisterL1ValidatorMessage{ + RegisterL1ValidatorMessage: addressedCall.Payload, + }, + } + return proto.Marshal(&justification) + } + } + } + } + } + } + return nil, fmt.Errorf("validation id %s not found on warp events", validationID) +} diff --git a/pkg/validatormanager/removal.go b/pkg/validatormanager/removal.go new file mode 100644 index 000000000..0af6a0d74 --- /dev/null +++ b/pkg/validatormanager/removal.go @@ -0,0 +1,261 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package validatormanager + +import ( + _ "embed" + "errors" + "math/big" + + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/sdk/interchain" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + warp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" +) + +func ValidatorManagerInitializeValidatorRemoval( + rpcURL string, + managerAddress common.Address, + ownerPrivateKey string, + validationID [32]byte, + isPoS bool, + force bool, +) (*types.Transaction, *types.Receipt, error) { + if isPoS { + // PoS only supports forcefull removal //TODO: implement uptime proof to remove this restriction + // posEndValidation := "initializeEndValidation(bytes32,bool,uint32)" + posEndValidation := "forceInitializeEndValidation(bytes32,bool,uint32)" + if force { + posEndValidation = "forceInitializeEndValidation(bytes32,bool,uint32)" + } + + return contract.TxToMethod( + rpcURL, + ownerPrivateKey, + managerAddress, + big.NewInt(0), + "POS validator removal initialization", + validatorManagerSDK.ErrorSignatureToError, + posEndValidation, + validationID, + false, // don't include uptime proof - rely on network to calculate uptime + uint32(0), + ) + } + // PoA case + return contract.TxToMethod( + rpcURL, + ownerPrivateKey, + managerAddress, + big.NewInt(0), + "POA validator removal initialization", + validatorManagerSDK.ErrorSignatureToError, + "initializeEndValidation(bytes32)", + validationID, + ) +} + +func ValidatorManagerGetSubnetValidatorWeightMessage( + network models.Network, + aggregatorLogLevel logging.Level, + aggregatorQuorumPercentage uint64, + aggregatorExtraPeerEndpoints []info.Peer, + subnetID ids.ID, + blockchainID ids.ID, + managerAddress common.Address, + validationID ids.ID, + nonce uint64, + weight uint64, +) (*warp.Message, error) { + addressedCallPayload, err := warpMessage.NewL1ValidatorWeight( + validationID, + nonce, + weight, + ) + if err != nil { + return nil, err + } + addressedCall, err := warpPayload.NewAddressedCall( + managerAddress.Bytes(), + addressedCallPayload.Bytes(), + ) + if err != nil { + return nil, err + } + unsignedMessage, err := warp.NewUnsignedMessage( + network.ID, + blockchainID, + addressedCall.Bytes(), + ) + if err != nil { + return nil, err + } + signatureAggregator, err := interchain.NewSignatureAggregator( + network, + aggregatorLogLevel, + subnetID, + aggregatorQuorumPercentage, + aggregatorExtraPeerEndpoints, + ) + if err != nil { + return nil, err + } + return signatureAggregator.Sign(unsignedMessage, nil) +} + +func InitValidatorRemoval( + app *application.Avalanche, + network models.Network, + rpcURL string, + chainSpec contract.ChainSpec, + ownerPrivateKey string, + nodeID ids.NodeID, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevelStr string, + initWithPos bool, + force bool, +) (*warp.Message, ids.ID, error) { + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return nil, ids.Empty, err + } + blockchainID, err := contract.GetBlockchainID( + app, + network, + chainSpec, + ) + if err != nil { + return nil, ids.Empty, err + } + managerAddress := common.HexToAddress(validatorManagerSDK.ProxyContractAddress) + validationID, err := GetRegisteredValidator( + rpcURL, + managerAddress, + nodeID, + ) + if err != nil { + return nil, ids.Empty, err + } + ux.Logger.PrintToUser("Using validationID: %s for nodeID: %s", validationID, nodeID) + tx, _, err := ValidatorManagerInitializeValidatorRemoval( + rpcURL, + managerAddress, + ownerPrivateKey, + validationID, + initWithPos, + force, + ) + if err != nil { + if !errors.Is(err, validatorManagerSDK.ErrInvalidValidatorStatus) { + return nil, ids.Empty, evm.TransactionError(tx, err, "failure initializing validator removal") + } + ux.Logger.PrintToUser("the validator removal process was already initialized. Proceeding to the next step") + } + + aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) + if err != nil { + aggregatorLogLevel = defaultAggregatorLogLevel + } + nonce := uint64(1) + signedMsg, err := ValidatorManagerGetSubnetValidatorWeightMessage( + network, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + subnetID, + blockchainID, + managerAddress, + validationID, + nonce, + 0, + ) + return signedMsg, validationID, err +} + +func ValidatorManagerCompleteValidatorRemoval( + rpcURL string, + managerAddress common.Address, + privateKey string, // not need to be owner atm + subnetValidatorRegistrationSignedMessage *warp.Message, +) (*types.Transaction, *types.Receipt, error) { + return contract.TxToMethodWithWarpMessage( + rpcURL, + privateKey, + managerAddress, + subnetValidatorRegistrationSignedMessage, + big.NewInt(0), + "complete poa validator removal", + validatorManagerSDK.ErrorSignatureToError, + "completeEndValidation(uint32)", + uint32(0), + ) +} + +func FinishValidatorRemoval( + app *application.Avalanche, + network models.Network, + rpcURL string, + chainSpec contract.ChainSpec, + privateKey string, + validationID ids.ID, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevelStr string, +) error { + managerAddress := common.HexToAddress(validatorManagerSDK.ProxyContractAddress) + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) + if err != nil { + aggregatorLogLevel = defaultAggregatorLogLevel + } + signedMessage, err := ValidatorManagerGetPChainSubnetValidatorRegistrationWarpMessage( + network, + rpcURL, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + subnetID, + validationID, + false, + ) + if err != nil { + return err + } + if err := evm.SetupProposerVM( + rpcURL, + privateKey, + ); err != nil { + return err + } + tx, _, err := ValidatorManagerCompleteValidatorRemoval( + rpcURL, + managerAddress, + privateKey, + signedMessage, + ) + if err != nil { + return evm.TransactionError(tx, err, "failure completing validator removal") + } + return nil +} diff --git a/pkg/validatormanager/validatormanager.go b/pkg/validatormanager/validatormanager.go new file mode 100644 index 000000000..4a0a70ef6 --- /dev/null +++ b/pkg/validatormanager/validatormanager.go @@ -0,0 +1,146 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package validatormanager + +import ( + _ "embed" + "math/big" + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/models" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/subnet-evm/core" + "github.com/ethereum/go-ethereum/common" +) + +const ( + defaultAggregatorLogLevel = logging.Off +) + +//go:embed deployed_poa_validator_manager_bytecode.txt +var deployedPoAValidatorManagerBytecode []byte + +func AddPoAValidatorManagerContractToAllocations( + allocs core.GenesisAlloc, +) { + deployedPoaValidatorManagerBytes := common.FromHex(strings.TrimSpace(string(deployedPoAValidatorManagerBytecode))) + allocs[common.HexToAddress(validatorManagerSDK.ValidatorContractAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedPoaValidatorManagerBytes, + Nonce: 1, + } +} + +//go:embed deployed_native_pos_validator_manager_bytecode.txt +var deployedPoSValidatorManagerBytecode []byte + +func AddPoSValidatorManagerContractToAllocations( + allocs core.GenesisAlloc, +) { + deployedPoSValidatorManagerBytes := common.FromHex(strings.TrimSpace(string(deployedPoSValidatorManagerBytecode))) + allocs[common.HexToAddress(validatorManagerSDK.ValidatorContractAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedPoSValidatorManagerBytes, + Nonce: 1, + } +} + +//go:embed deployed_transparent_proxy_bytecode.txt +var deployedTransparentProxyBytecode []byte + +//go:embed deployed_proxy_admin_bytecode.txt +var deployedProxyAdminBytecode []byte + +func AddTransparentProxyContractToAllocations( + allocs core.GenesisAlloc, + proxyManager string, +) { + // proxy admin + deployedProxyAdmin := common.FromHex(strings.TrimSpace(string(deployedProxyAdminBytecode))) + allocs[common.HexToAddress(validatorManagerSDK.ProxyAdminContractAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedProxyAdmin, + Nonce: 1, + Storage: map[common.Hash]common.Hash{ + common.HexToHash("0x0"): common.HexToHash(proxyManager), + }, + } + + // transparent proxy + deployedTransparentProxy := common.FromHex(strings.TrimSpace(string(deployedTransparentProxyBytecode))) + allocs[common.HexToAddress(validatorManagerSDK.ProxyContractAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedTransparentProxy, + Nonce: 1, + Storage: map[common.Hash]common.Hash{ + common.HexToHash("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"): common.HexToHash(validatorManagerSDK.ValidatorContractAddress), // sslot for address of ValidatorManager logic -> bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) + common.HexToHash("0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"): common.HexToHash(validatorManagerSDK.ProxyAdminContractAddress), // sslot for address of ProxyAdmin -> bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) + // we can omit 3rd sslot for _data, as we initialize ValidatorManager after chain is live + }, + } +} + +//go:embed deployed_reward_calculator_bytecode.txt +var deployedRewardCalculatorBytecode []byte + +func AddRewardCalculatorToAllocations( + allocs core.GenesisAlloc, + rewardBasisPoints uint64, +) { + deployedRewardCalculatorBytes := common.FromHex(strings.TrimSpace(string(deployedRewardCalculatorBytecode))) + allocs[common.HexToAddress(validatorManagerSDK.RewardCalculatorAddress)] = core.GenesisAccount{ + Balance: big.NewInt(0), + Code: deployedRewardCalculatorBytes, + Nonce: 1, + Storage: map[common.Hash]common.Hash{ + common.HexToHash("0x0"): common.BigToHash(new(big.Int).SetUint64(rewardBasisPoints)), + }, + } +} + +// setups PoA manager after a successful execution of +// ConvertSubnetTx on P-Chain +// needs the list of validators for that tx, +// [convertSubnetValidators], together with an evm [ownerAddress] +// to set as the owner of the PoA manager +func SetupPoA( + subnet blockchainSDK.Subnet, + network models.Network, + privateKey string, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevelStr string, +) error { + aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) + if err != nil { + aggregatorLogLevel = defaultAggregatorLogLevel + } + return subnet.InitializeProofOfAuthority(network, privateKey, aggregatorExtraPeerEndpoints, aggregatorLogLevel) +} + +// setups PoA manager after a successful execution of +// ConvertSubnetTx on P-Chain +// needs the list of validators for that tx, +// [convertSubnetValidators], together with an evm [ownerAddress] +// to set as the owner of the PoA manager +func SetupPoS( + subnet blockchainSDK.Subnet, + network models.Network, + privateKey string, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevelStr string, + posParams validatorManagerSDK.PoSParams, +) error { + aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) + if err != nil { + aggregatorLogLevel = defaultAggregatorLogLevel + } + return subnet.InitializeProofOfStake(network, + privateKey, + aggregatorExtraPeerEndpoints, + aggregatorLogLevel, + posParams, + ) +} diff --git a/pkg/vm/allowlist.go b/pkg/vm/allowlist.go index 536c7ace1..53851a1bb 100644 --- a/pkg/vm/allowlist.go +++ b/pkg/vm/allowlist.go @@ -98,6 +98,7 @@ func removeAddress( func GenerateAllowList( app *application.Avalanche, + allowList AllowList, action string, evmVersion string, ) (AllowList, bool, error) { @@ -106,8 +107,6 @@ func GenerateAllowList( } managerRoleEnabled := semver.Compare(evmVersion, "v0.6.4") >= 0 - allowList := AllowList{} - promptTemplate := "Configure the addresses that are allowed to %s" prompt := fmt.Sprintf(promptTemplate, action) @@ -122,6 +121,12 @@ func GenerateAllowList( enabledOption := "Enabled" explainOption := "Explain the difference" + if len(allowList.AdminAddresses) != 0 || len(allowList.ManagerAddresses) != 0 || len(allowList.EnabledAddresses) != 0 { + fmt.Println() + fmt.Printf(logging.Bold.Wrap("Addresses automatically allowed to %s\n"), action) + preview(allowList) + } + for { options := []string{addOption, removeOption, previewOption, confirmOption, cancelOption} if len(allowList.AdminAddresses) == 0 && len(allowList.ManagerAddresses) == 0 && len(allowList.EnabledAddresses) == 0 { diff --git a/pkg/vm/create_custom.go b/pkg/vm/create_custom.go index 5834abc85..a49e6a2a5 100644 --- a/pkg/vm/create_custom.go +++ b/pkg/vm/create_custom.go @@ -17,6 +17,7 @@ import ( ) func CreateCustomSidecar( + sc *models.Sidecar, app *application.Avalanche, subnetName string, useRepo bool, @@ -25,15 +26,18 @@ func CreateCustomSidecar( customVMBuildScript string, vmPath string, tokenSymbol string, + sovereign bool, ) (*models.Sidecar, error) { ux.Logger.PrintToUser("creating custom VM subnet %s", subnetName) - sc := &models.Sidecar{ - Name: subnetName, - VM: models.CustomVM, - Subnet: subnetName, + if sc == nil { + sc = &models.Sidecar{} } + sc.Name = subnetName + sc.VM = models.CustomVM + sc.Subnet = subnetName + if tokenSymbol != "" { sc.TokenSymbol = tokenSymbol sc.TokenName = tokenSymbol + " Token" @@ -48,38 +52,38 @@ func CreateCustomSidecar( options := []string{githubOption, localOption} option, err := app.Prompt.CaptureList("How do you want to set up the VM binary?", options) if err != nil { - return &models.Sidecar{}, err + return nil, err } if option == githubOption { useRepo = true } else { vmPath, err = app.Prompt.CaptureExistingFilepath("Enter path to VM binary") if err != nil { - return &models.Sidecar{}, err + return nil, err } } } if useRepo { if err := SetCustomVMSourceCodeFields(app, sc, customVMRepoURL, customVMBranch, customVMBuildScript); err != nil { - return &models.Sidecar{}, err + return nil, err } if err := BuildCustomVM(app, sc); err != nil { - return &models.Sidecar{}, err + return nil, err } vmPath = app.GetCustomVMPath(subnetName) } else { if err := app.CopyVMBinary(vmPath, subnetName); err != nil { - return &models.Sidecar{}, err + return nil, err } } rpcVersion, err := GetVMBinaryProtocolVersion(vmPath) if err != nil { - return &models.Sidecar{}, fmt.Errorf("unable to get RPC version: %w", err) + return nil, fmt.Errorf("unable to get RPC version: %w", err) } sc.RPCVersion = rpcVersion - + sc.Sovereign = sovereign return sc, nil } diff --git a/pkg/vm/create_evm.go b/pkg/vm/create_evm.go index b25092c3d..12427babc 100644 --- a/pkg/vm/create_evm.go +++ b/pkg/vm/create_evm.go @@ -14,7 +14,8 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/teleporter" - "github.com/ava-labs/avalanche-cli/pkg/ux" + icmgenesis "github.com/ava-labs/avalanche-cli/pkg/teleporter/genesis" + "github.com/ava-labs/avalanche-cli/pkg/validatormanager" blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/utils" @@ -31,17 +32,23 @@ var ( ) func CreateEvmSidecar( + sc *models.Sidecar, app *application.Avalanche, subnetName string, subnetEVMVersion string, tokenSymbol string, getRPCVersionFromBinary bool, + sovereign bool, ) (*models.Sidecar, error) { var ( err error rpcVersion int ) + if sc == nil { + sc = &models.Sidecar{} + } + if getRPCVersionFromBinary { _, vmBin, err := binutils.SetupSubnetEVM(app, subnetEVMVersion) if err != nil { @@ -58,26 +65,24 @@ func CreateEvmSidecar( } } - sc := models.Sidecar{ - Name: subnetName, - VM: models.SubnetEvm, - VMVersion: subnetEVMVersion, - RPCVersion: rpcVersion, - Subnet: subnetName, - TokenSymbol: tokenSymbol, - TokenName: tokenSymbol + " Token", - } - - return &sc, nil + sc.Name = subnetName + sc.VM = models.SubnetEvm + sc.VMVersion = subnetEVMVersion + sc.RPCVersion = rpcVersion + sc.Subnet = subnetName + sc.TokenSymbol = tokenSymbol + sc.TokenName = tokenSymbol + " Token" + sc.Sovereign = sovereign + return sc, nil } func CreateEVMGenesis( - blockchainName string, params SubnetEVMGenesisParams, teleporterInfo *teleporter.Info, + addICMRegistryToGenesis bool, + proxyOwner string, + rewardBasisPoints uint64, ) ([]byte, error) { - ux.Logger.PrintToUser("creating genesis for blockchain %s", blockchainName) - feeConfig := getFeeConfig(params) // Validity checks on the parameter settings. @@ -106,6 +111,26 @@ func CreateEVMGenesis( params.initialTokenAllocation[common.HexToAddress(teleporterInfo.FundedAddress)] = core.GenesisAccount{ Balance: balance, } + icmgenesis.AddICMMessengerContractToAllocations(params.initialTokenAllocation) + if addICMRegistryToGenesis { + // experimental + if err := icmgenesis.AddICMRegistryContractToAllocations(params.initialTokenAllocation); err != nil { + return nil, err + } + } + } + + if params.UsePoAValidatorManager && params.UsePoSValidatorManager { + return nil, fmt.Errorf("blockchain can not be both PoA and PoS") + } + if params.UsePoAValidatorManager { + validatormanager.AddPoAValidatorManagerContractToAllocations(params.initialTokenAllocation) + validatormanager.AddTransparentProxyContractToAllocations(params.initialTokenAllocation, proxyOwner) + } else if params.UsePoSValidatorManager { + validatormanager.AddPoSValidatorManagerContractToAllocations(params.initialTokenAllocation) + validatormanager.AddTransparentProxyContractToAllocations(params.initialTokenAllocation, proxyOwner) + validatormanager.AddRewardCalculatorToAllocations(params.initialTokenAllocation, rewardBasisPoints) + params.enableNativeMinterPrecompile = true } if params.UseExternalGasToken { diff --git a/pkg/vm/evm_prompts.go b/pkg/vm/evm_prompts.go index be886b2ab..3f67e52ab 100644 --- a/pkg/vm/evm_prompts.go +++ b/pkg/vm/evm_prompts.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" + validatorManagerSDK "github.com/ava-labs/avalanche-cli/sdk/validatormanager" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/plugin/evm" "github.com/ethereum/go-ethereum/common" @@ -52,12 +53,6 @@ const ( confirmAddressAllocationOption = "Confirm and finalize the initial token allocation" ) -var DefaultEwoqAllocation = core.GenesisAlloc{ - PrefundedEwoqAddress: { - Balance: defaultEVMAirdropAmount, - }, -} - type FeeConfig struct { lowThroughput bool mediumThroughput bool @@ -90,6 +85,8 @@ type SubnetEVMGenesisParams struct { enableContractDeployerPrecompile bool contractDeployerPrecompileAllowList AllowList enableWarpPrecompile bool + UsePoAValidatorManager bool + UsePoSValidatorManager bool } func PromptTokenSymbol( @@ -160,6 +157,7 @@ func PromptVMType( // provided in call args func PromptSubnetEVMGenesisParams( app *application.Avalanche, + sc *models.Sidecar, version string, chainID uint64, tokenSymbol string, @@ -173,6 +171,25 @@ func PromptSubnetEVMGenesisParams( err error params SubnetEVMGenesisParams ) + params.initialTokenAllocation = core.GenesisAlloc{} + + if sc.PoA() { + params.UsePoAValidatorManager = true + params.initialTokenAllocation[common.HexToAddress(sc.ValidatorManagerOwner)] = core.GenesisAccount{ + Balance: defaultPoAOwnerBalance, + } + } + + if sc.PoS() { + params.UsePoSValidatorManager = true + + params.enableNativeMinterPrecompile = true + params.nativeMinterPrecompileAllowList.EnabledAddresses = append( + params.nativeMinterPrecompileAllowList.EnabledAddresses, + common.HexToAddress(validatorManagerSDK.ProxyContractAddress), + ) + params.enableRewardManagerPrecompile = true + } // Chain ID params.chainID = chainID @@ -221,6 +238,10 @@ func PromptSubnetEVMGenesisParams( return SubnetEVMGenesisParams{}, "", err } + if sc.PoS() || sc.PoA() { // Teleporter bytecode makes genesis too big given the current max size (we include the bytecode for ValidatorManager, a proxy, and proxy admin) + params.UseTeleporter = false + } + return params, tokenSymbol, nil } @@ -321,47 +342,57 @@ func displayAllocations(alloc core.GenesisAlloc) { table.Render() } -func createNewKeyAllocation(app *application.Avalanche, subnetName string) (core.GenesisAlloc, error) { +func addNewKeyAllocation(allocations core.GenesisAlloc, app *application.Avalanche, subnetName string) error { keyName := utils.GetDefaultBlockchainAirdropKeyName(subnetName) k, err := app.GetKey(keyName, models.NewLocalNetwork(), true) if err != nil { - return core.GenesisAlloc{}, err + return err } ux.Logger.PrintToUser("prefunding address %s with balance %s", k.C(), defaultEVMAirdropAmount) + allocations[common.HexToAddress(k.C())] = core.GenesisAccount{ + Balance: defaultEVMAirdropAmount, + } + return nil +} - return core.GenesisAlloc{ - common.HexToAddress(k.C()): { - Balance: defaultEVMAirdropAmount, - }, - }, nil +func addEwoqAllocation(allocations core.GenesisAlloc) { + allocations[PrefundedEwoqAddress] = core.GenesisAccount{ + Balance: defaultEVMAirdropAmount, + } } func getNativeGasTokenAllocationConfig( + allocations core.GenesisAlloc, app *application.Avalanche, subnetName string, tokenSymbol string, -) (core.GenesisAlloc, error) { +) error { // Get the type of initial token allocation from the user prompt. allocOption, err := app.Prompt.CaptureList( "How should the initial token allocation be structured?", []string{allocateToNewKeyOption, allocateToEwoqOption, customAllocationOption}, ) if err != nil { - return core.GenesisAlloc{}, err + return err } // If the user chooses to allocate to a new key, generate a new key and allocate the default amount to it. if allocOption == allocateToNewKeyOption { - return createNewKeyAllocation(app, subnetName) + return addNewKeyAllocation(allocations, app, subnetName) } if allocOption == allocateToEwoqOption { ux.Logger.PrintToUser("prefunding address %s with balance %s", PrefundedEwoqAddress, defaultEVMAirdropAmount) - return DefaultEwoqAllocation, nil + addEwoqAllocation(allocations) + return nil } if allocOption == customAllocationOption { - res := core.GenesisAlloc{} + if len(allocations) != 0 { + fmt.Println() + fmt.Println(logging.Bold.Wrap("Addresses automatically allocated")) + displayAllocations(allocations) + } for { // Prompt for the action the user wants to take on the allocation list. action, err := app.Prompt.CaptureList( @@ -375,108 +406,118 @@ func getNativeGasTokenAllocationConfig( }, ) if err != nil { - return core.GenesisAlloc{}, err + return err } switch action { case addAddressAllocationOption: address, err := app.Prompt.CaptureAddress("Address to allocate to") if err != nil { - return core.GenesisAlloc{}, err + return err } // Check if the address already has an allocation entry. - if _, ok := res[address]; ok { + if _, ok := allocations[address]; ok { ux.Logger.PrintToUser("Address already has an allocation entry. Use edit or remove to modify.") continue } balance, err := app.Prompt.CaptureUint64(fmt.Sprintf("Amount to allocate (in %s units)", tokenSymbol)) if err != nil { - return core.GenesisAlloc{}, err + return err } - res[address] = core.GenesisAccount{ + allocations[address] = core.GenesisAccount{ Balance: new(big.Int).Mul(new(big.Int).SetUint64(balance), OneAvax), } case changeAddressAllocationOption: address, err := app.Prompt.CaptureAddress("Address to update the allocation of") if err != nil { - return core.GenesisAlloc{}, err + return err } // Check the address has an existing allocation entry. - if _, ok := res[address]; !ok { + if _, ok := allocations[address]; !ok { ux.Logger.PrintToUser("Address not found in the allocation list") continue } balance, err := app.Prompt.CaptureUint64(fmt.Sprintf("Updated amount to allocate (in %s units)", tokenSymbol)) if err != nil { - return core.GenesisAlloc{}, err + return err } - res[address] = core.GenesisAccount{ + allocations[address] = core.GenesisAccount{ Balance: new(big.Int).Mul(new(big.Int).SetUint64(balance), OneAvax), } case removeAddressAllocationOption: address, err := app.Prompt.CaptureAddress("Address to remove from the allocation list") if err != nil { - return core.GenesisAlloc{}, err + return err } // Check the address has an existing allocation entry. - if _, ok := res[address]; !ok { + if _, ok := allocations[address]; !ok { ux.Logger.PrintToUser("Address not found in the allocation list") continue } - delete(res, address) + delete(allocations, address) case previewAddressAllocationOption: - displayAllocations(res) + displayAllocations(allocations) case confirmAddressAllocationOption: - displayAllocations(res) + displayAllocations(allocations) confirm, err := app.Prompt.CaptureYesNo("Are you sure you want to finalize this allocation list?") if err != nil { - return core.GenesisAlloc{}, err + return err } if confirm { - return res, nil + return nil } default: - return core.GenesisAlloc{}, fmt.Errorf("invalid allocation modification option") + return fmt.Errorf("invalid allocation modification option") } } } - return core.GenesisAlloc{}, fmt.Errorf("invalid allocation option") + return fmt.Errorf("invalid allocation option") } -func getNativeMinterPrecompileConfig(app *application.Avalanche, version string) (AllowList, bool, error) { - option, err := app.Prompt.CaptureList( - "Allow minting of new native tokens?", - []string{fixedSupplyOption, dynamicSupplyOption}, - ) - if err != nil { - return AllowList{}, false, err - } - - if option == fixedSupplyOption { - return AllowList{}, false, nil +func getNativeMinterPrecompileConfig( + app *application.Avalanche, + alreadyEnabled bool, + allowList AllowList, + version string, +) (AllowList, bool, error) { + if !alreadyEnabled { + option, err := app.Prompt.CaptureList( + "Allow minting of new native tokens?", + []string{fixedSupplyOption, dynamicSupplyOption}, + ) + if err != nil { + return AllowList{}, false, err + } + if option == fixedSupplyOption { + return AllowList{}, false, nil + } + } else { + confirm, err := app.Prompt.CaptureYesNo("Minting of native tokens automatically enabled. Do you want to configure allow list?") + if err != nil { + return AllowList{}, false, err + } + if !confirm { + return AllowList{}, false, nil + } } - if option == dynamicSupplyOption { - for { - allowList, cancel, err := GenerateAllowList(app, "mint native tokens", version) - if err != nil { - return AllowList{}, false, err - } - if cancel { - continue - } - return allowList, true, nil + for { + allowList, cancel, err := GenerateAllowList(app, allowList, "mint native tokens", version) + if err != nil { + return AllowList{}, false, err } + if cancel { + continue + } + return allowList, true, nil } - - return AllowList{}, false, fmt.Errorf("invalid option") } // prompts for token symbol, initial token allocation, and native minter precompile @@ -502,27 +543,30 @@ func promptNativeGasToken( if defaultsKind == TestDefaults { ux.Logger.PrintToUser("prefunding address %s with balance %s", PrefundedEwoqAddress, defaultEVMAirdropAmount) - params.initialTokenAllocation = DefaultEwoqAllocation + addEwoqAllocation(params.initialTokenAllocation) return params, tokenSymbol, nil } if defaultsKind == ProductionDefaults { - params.initialTokenAllocation, err = createNewKeyAllocation(app, blockchainName) + err = addNewKeyAllocation(params.initialTokenAllocation, app, blockchainName) return params, tokenSymbol, err } // No defaults case. Prompt for initial token allocation and native minter precompile options. - alloc, err := getNativeGasTokenAllocationConfig(app, blockchainName, tokenSymbol) - if err != nil { + if err := getNativeGasTokenAllocationConfig(params.initialTokenAllocation, app, blockchainName, tokenSymbol); err != nil { return SubnetEVMGenesisParams{}, "", err } - allowList, nativeMinterEnabled, err := getNativeMinterPrecompileConfig(app, version) + allowList, nativeMinterEnabled, err := getNativeMinterPrecompileConfig( + app, + params.enableNativeMinterPrecompile, + params.nativeMinterPrecompileAllowList, + version, + ) if err != nil { return SubnetEVMGenesisParams{}, "", err } - params.initialTokenAllocation = alloc params.enableNativeMinterPrecompile = nativeMinterEnabled params.nativeMinterPrecompileAllowList = allowList return params, tokenSymbol, nil @@ -656,7 +700,7 @@ func promptFeeConfig( switch option { case dontChangeFeeSettingsOption: case changeFeeSettingsOption: - params.feeManagerPrecompileAllowList, cancel, err = GenerateAllowList(app, "adjust the gas fees", version) + params.feeManagerPrecompileAllowList, cancel, err = GenerateAllowList(app, AllowList{}, "adjust the gas fees", version) if err != nil { return SubnetEVMGenesisParams{}, err } @@ -684,7 +728,7 @@ func promptFeeConfig( switch option { case burnFees: case distributeFees: - params.rewardManagerPrecompileAllowList, cancel, err = GenerateAllowList(app, "customize gas fees distribution", version) + params.rewardManagerPrecompileAllowList, cancel, err = GenerateAllowList(app, AllowList{}, "customize gas fees distribution", version) if err != nil { return SubnetEVMGenesisParams{}, err } @@ -779,7 +823,7 @@ func promptPermissioning( } switch option { case approvedCanSubmitTransactionsOption: - params.transactionPrecompileAllowList, cancel, err = GenerateAllowList(app, "issue transactions", version) + params.transactionPrecompileAllowList, cancel, err = GenerateAllowList(app, AllowList{}, "issue transactions", version) if err != nil { return SubnetEVMGenesisParams{}, err } @@ -808,7 +852,7 @@ func promptPermissioning( } switch option { case approvedCanDeployContractsOption: - params.contractDeployerPrecompileAllowList, cancel, err = GenerateAllowList(app, "deploy smart contracts", version) + params.contractDeployerPrecompileAllowList, cancel, err = GenerateAllowList(app, AllowList{}, "deploy smart contracts", version) if err != nil { return SubnetEVMGenesisParams{}, err } diff --git a/pkg/vm/evm_settings.go b/pkg/vm/evm_settings.go index 26e0d1899..1523102fd 100644 --- a/pkg/vm/evm_settings.go +++ b/pkg/vm/evm_settings.go @@ -29,4 +29,5 @@ var ( OneAvax = new(big.Int).SetUint64(1000000000000000000) defaultEVMAirdropAmount = new(big.Int).Exp(big.NewInt(10), big.NewInt(24), nil) // 10^24 + defaultPoAOwnerBalance = new(big.Int).Mul(OneAvax, big.NewInt(10)) // 10 Native Tokens ) diff --git a/pkg/vm/precompiles.go b/pkg/vm/precompiles.go index ee32c5a64..6bd199831 100644 --- a/pkg/vm/precompiles.go +++ b/pkg/vm/precompiles.go @@ -99,7 +99,8 @@ func configureRewardManager( func configureWarp(timestamp *uint64) warp.Config { return warp.Config{ - QuorumNumerator: warp.WarpDefaultQuorumNumerator, + QuorumNumerator: warp.WarpDefaultQuorumNumerator, + RequirePrimaryNetworkSigners: true, Upgrade: precompileconfig.Upgrade{ BlockTimestamp: timestamp, }, diff --git a/scripts/regenerate_mocks.sh b/scripts/regenerate_mocks.sh index 6537a1d31..f94eee0a7 100755 --- a/scripts/regenerate_mocks.sh +++ b/scripts/regenerate_mocks.sh @@ -14,11 +14,11 @@ if ! [[ "$0" =~ scripts/regenerate_mocks.sh ]]; then exit 1 fi -go install github.com/vektra/mockery/v2@latest +go install github.com/vektra/mockery/v2@v2.43.2 -mockery -r --output ./internal/mocks --name BinaryChecker --filename binaryChecker.go -mockery -r --output ./internal/mocks --name PluginBinaryDownloader --filename pluginBinaryDownloader.go -mockery -r --output ./internal/mocks --name ProcessChecker --filename processChecker.go +mockery -r --output ./internal/mocks --name BinaryChecker --filename binary_checker.go +mockery -r --output ./internal/mocks --name PluginBinaryDownloader --filename plugin_binary_downloader.go +mockery -r --output ./internal/mocks --name ProcessChecker --filename process_checker.go mockery -r --output ./internal/mocks --name Prompter --filename prompter.go mockery -r --output ./internal/mocks --name Installer --filename installer.go mockery -r --output ./internal/mocks --name Publisher --filename publisher.go diff --git a/scripts/unit_test.sh b/scripts/unit_test.sh new file mode 100755 index 000000000..a9d9b2b57 --- /dev/null +++ b/scripts/unit_test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +go test -v -coverprofile=coverage.out $(go list ./... | grep -v /tests/ | grep -v '/sdk/') +#go tool cover -func=coverage.out diff --git a/sdk/blockchain/blockchain.go b/sdk/blockchain/blockchain.go index 64324a1c3..4511e8b6c 100644 --- a/sdk/blockchain/blockchain.go +++ b/sdk/blockchain/blockchain.go @@ -12,6 +12,14 @@ import ( "os" "time" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ethereum/go-ethereum/common" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanche-cli/sdk/multisig" @@ -27,6 +35,14 @@ import ( "github.com/ava-labs/subnet-evm/params" ) +var ( + errMissingSubnetID = fmt.Errorf("missing Subnet ID") + errMissingBlockchainID = fmt.Errorf("missing Blockchain ID") + errMissingRPC = fmt.Errorf("missing RPC URL") + errMissingBootstrapValidators = fmt.Errorf("missing bootstrap validators") + errMissingOwnerAddress = fmt.Errorf("missing Owner Address") +) + type SubnetParams struct { // File path of Genesis to use // Do not set SubnetEVMParams or CustomVMParams @@ -101,11 +117,24 @@ type Subnet struct { // the target Subnet for CreateChainTx and AddValidatorTx SubnetID ids.ID + // BlockchainID is the transaction ID from an issued CreateChainTx + BlockchainID ids.ID + // VMID specifies the vm that the new chain will run when CreateChainTx is called VMID ids.ID // DeployInfo contains all the necessary information for createSubnetTx DeployInfo DeployParams + + // RPC URL that Subnet can be reached at + RPC string + + // OwnerAddress is address of the owner of the Validator Manager Contract + OwnerAddress *common.Address + + // BootstrapValidators are bootstrap validators that are included in the ConvertL1Tx call + // that made Subnet a sovereign blockchain + BootstrapValidators []*txs.ConvertSubnetToL1Validator } func (c *Subnet) SetParams(controlKeys []ids.ShortID, subnetAuthKeys []ids.ShortID, threshold uint32) { @@ -306,3 +335,142 @@ func (c *Subnet) Commit(ms multisig.Multisig, wallet wallet.Wallet, waitForTxAcc } return tx.ID(), issueTxErr } + +// InitializeProofOfAuthority setups PoA manager after a successful execution of +// ConvertSubnetTx on P-Chain +// needs the list of validators for that tx, +// [convertSubnetValidators], together with an evm [ownerAddress] +// to set as the owner of the PoA manager +func (c *Subnet) InitializeProofOfAuthority( + network models.Network, + privateKey string, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevel logging.Level, +) error { + if c.SubnetID == ids.Empty { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingSubnetID) + } + + if c.BlockchainID == ids.Empty { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingBlockchainID) + } + + if c.RPC == "" { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingRPC) + } + + if c.OwnerAddress == nil { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingOwnerAddress) + } + + if len(c.BootstrapValidators) == 0 { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingBootstrapValidators) + } + + if err := evm.SetupProposerVM( + c.RPC, + privateKey, + ); err != nil { + return err + } + + managerAddress := common.HexToAddress(validatormanager.ProxyContractAddress) + tx, _, err := validatormanager.PoAValidatorManagerInitialize( + c.RPC, + managerAddress, + privateKey, + c.SubnetID, + *c.OwnerAddress, + ) + if err != nil { + if !errors.Is(err, validatormanager.ErrAlreadyInitialized) { + return evm.TransactionError(tx, err, "failure initializing poa validator manager") + } + ux.Logger.PrintToUser("Warning: the PoA contract is already initialized.") + } + + subnetConversionSignedMessage, err := validatormanager.GetPChainSubnetConversionWarpMessage( + network, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + c.SubnetID, + c.BlockchainID, + managerAddress, + c.BootstrapValidators, + ) + if err != nil { + return fmt.Errorf("failure signing subnet conversion warp message: %w", err) + } + + tx, _, err = validatormanager.InitializeValidatorsSet( + c.RPC, + managerAddress, + privateKey, + c.SubnetID, + c.BlockchainID, + c.BootstrapValidators, + subnetConversionSignedMessage, + ) + if err != nil { + return evm.TransactionError(tx, err, "failure initializing validators set on poa manager") + } + + return nil +} + +func (c *Subnet) InitializeProofOfStake( + network models.Network, + privateKey string, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevel logging.Level, + posParams validatormanager.PoSParams, +) error { + if err := evm.SetupProposerVM( + c.RPC, + privateKey, + ); err != nil { + return err + } + managerAddress := common.HexToAddress(validatormanager.ProxyContractAddress) + tx, _, err := validatormanager.PoSValidatorManagerInitialize( + c.RPC, + managerAddress, + privateKey, + c.SubnetID, + posParams, + ) + if err != nil { + if !errors.Is(err, validatormanager.ErrAlreadyInitialized) { + return evm.TransactionError(tx, err, "failure initializing native PoS validator manager") + } + ux.Logger.PrintToUser("Warning: the PoS contract is already initialized.") + } + subnetConversionSignedMessage, err := validatormanager.GetPChainSubnetConversionWarpMessage( + network, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + c.SubnetID, + c.BlockchainID, + managerAddress, + c.BootstrapValidators, + ) + if err != nil { + return fmt.Errorf("failure signing subnet conversion warp message: %w", err) + } + + tx, _, err = validatormanager.InitializeValidatorsSet( + c.RPC, + managerAddress, + privateKey, + c.SubnetID, + c.BlockchainID, + c.BootstrapValidators, + subnetConversionSignedMessage, + ) + if err != nil { + return evm.TransactionError(tx, err, "failure initializing validators set on pos manager") + } + return nil +} diff --git a/sdk/interchain/README.md b/sdk/interchain/README.md new file mode 100644 index 000000000..b01908746 --- /dev/null +++ b/sdk/interchain/README.md @@ -0,0 +1,58 @@ +# Signature-aggregator SDK + +## Usage example + +``` +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved +// See the file LICENSE for licensing terms. + +package main + +import ( + "fmt" + "os" + + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/sdk/interchain" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func main() { + network := models.NewFujiNetwork() + aggregator, err := interchain.NewSignatureAggregator( + network, + logging.NewLogger( + "aggregator_test", + logging.NewWrappedCore( + logging.Error, + os.Stdout, + zapcore.NewConsoleEncoder( + zap.NewProductionEncoderConfig(), + ), + ), + ), + logging.Error, + ids.Empty, + interchain.DefaultQuorumPercentage, + ) + if err != nil { + panic(err) + } + + msgHexString := "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002cc0000000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5000002a200000000000100000014253b2784c75e510dd0ff1da844684a1ac0aa5fcf000002800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000049d0000000000000000000000009297e09b81db7ae921fb69dc745dc0d37ec4ab6ddd30039ae180ccb3b132f5c67f299b83749f1f0a12f7d69878c6b696598fd0c50000000000000000000000006839ebb9c7b7df12a2147790f6aaa19ac1bf76eb000000000000000000000000000000000000000000000000000000000001117000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000600000000000000000000000014c3ae49cb46c965e6c9aca4a121c8f8481d44b900000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f7571ae49399c8a1b4c8c8eb8cc328bc5f817590000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000" + justificationHexString := hex.EncodeToString([]byte("test")) + reply, err := aggregator.AggregateSignatures(msgHexString, justificationHexString) + if err != nil { + panic(err) + } + fmt.Println(reply) +} +``` + +``` +❯ go run example.go +0000000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5000002a200000000000100000014253b2784c75e510dd0ff1da844684a1ac0aa5fcf000002800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000049d0000000000000000000000009297e09b81db7ae921fb69dc745dc0d37ec4ab6ddd30039ae180ccb3b132f5c67f299b83749f1f0a12f7d69878c6b696598fd0c50000000000000000000000006839ebb9c7b7df12a2147790f6aaa19ac1bf76eb000000000000000000000000000000000000000000000000000000000001117000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000600000000000000000000000014c3ae49cb46c965e6c9aca4a121c8f8481d44b900000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f7571ae49399c8a1b4c8c8eb8cc328bc5f817590000000000000000000000000000000000000000000000000016345785d8a00000000000000000036210300000084000102000a10100100000402000000001204000020000000000000000030100040000400130444001000084011042040aa495650029c5a7106a75623ab55ab9e895795f79d12a5f5b62ce1f55c71989f3bc70a7ddbfdf520953c0d9cfe1329b60e6e931a9f548da3101d435773bdb5756cbdb1a8cc25c08f7dba367642a6c35cfbb3d817d93fb7948e8038d0cb06414f +``` diff --git a/sdk/interchain/signature-aggregator.go b/sdk/interchain/signature-aggregator.go new file mode 100644 index 000000000..951de2555 --- /dev/null +++ b/sdk/interchain/signature-aggregator.go @@ -0,0 +1,214 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved +// See the file LICENSE for licensing terms. +package interchain + +import ( + "encoding/hex" + "fmt" + "os" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/message" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + apiConfig "github.com/ava-labs/awm-relayer/config" + "github.com/ava-labs/awm-relayer/peers" + "github.com/ava-labs/awm-relayer/signature-aggregator/aggregator" + "github.com/ava-labs/awm-relayer/signature-aggregator/config" + "github.com/ava-labs/awm-relayer/signature-aggregator/metrics" + awmTypes "github.com/ava-labs/awm-relayer/types" + awmUtils "github.com/ava-labs/awm-relayer/utils" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + DefaultQuorumPercentage = uint64(67) + DefaultSignatureCacheSize = uint64(1024 * 1024) +) + +var etnaTime = time.Unix(0, 0) + +type SignatureAggregator struct { + subnetID ids.ID + quorumPercentage uint64 + aggregator *aggregator.SignatureAggregator +} + +// createAppRequestNetwork creates a new AppRequestNetwork for the given network and log level. +// +// Parameters: +// - network: The network for which the AppRequestNetwork is created. It should be of type models.Network. +// - logLevel: The log level for the AppRequestNetwork. It should be of type logging.Level. +// +// Returns: +// - peers.AppRequestNetwork: The created AppRequestNetwork, or nil if an error occurred. +// - error: An error if the creation of the AppRequestNetwork failed. +func createAppRequestNetwork( + network models.Network, + logLevel logging.Level, + registerer prometheus.Registerer, + extraPeerEndpoints []info.Peer, +) (peers.AppRequestNetwork, error) { + peerNetwork, err := peers.NewNetwork( + logLevel, + registerer, + nil, + extraPeerEndpoints, + &config.Config{ + PChainAPI: &apiConfig.APIConfig{ + BaseURL: network.Endpoint, + }, + InfoAPI: &apiConfig.APIConfig{ + BaseURL: network.Endpoint, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create peer network: %w", err) + } + return peerNetwork, nil +} + +// initSignatureAggregator initializes a new SignatureAggregator instance. +// +// network is the network to create the aggregator for. +// logger is the logger to use for logging. +// subnetID is the subnet ID to create the aggregator for. +// quorumPercentage is the quorum percentage to use for the aggregator. +// +// Returns a new SignatureAggregator instance, or an error if initialization fails. +func initSignatureAggregator( + network peers.AppRequestNetwork, + logger logging.Logger, + registerer prometheus.Registerer, + subnetID ids.ID, + quorumPercentage uint64, +) (*SignatureAggregator, error) { + sa := &SignatureAggregator{} + // set quorum percentage + sa.quorumPercentage = quorumPercentage + if quorumPercentage == 0 { + sa.quorumPercentage = DefaultQuorumPercentage + } else if quorumPercentage > 100 { + return nil, fmt.Errorf("quorum percentage cannot be greater than 100") + } + sa.subnetID = subnetID + + messageCreator, err := message.NewCreator( + logger, + registerer, + constants.DefaultNetworkCompressionType, + constants.DefaultNetworkMaximumInboundTimeout, + ) + if err != nil { + return nil, fmt.Errorf("failed to create message creator: %w", err) + } + + metricsInstance := metrics.NewSignatureAggregatorMetrics(registerer) + signatureAggregator, err := aggregator.NewSignatureAggregator( + network, + logger, + DefaultSignatureCacheSize, + metricsInstance, + messageCreator, + etnaTime, + ) + if err != nil { + return nil, fmt.Errorf("failed to create signature aggregator: %w", err) + } + sa.aggregator = signatureAggregator + return sa, nil +} + +// NewSignatureAggregator creates a new signature aggregator instance. +// +// network is the network to create the aggregator for. +// logger is the logger to use for logging. +// logLevel is the log level to use for logging. +// subnetID is the subnet ID to create the aggregator for. +// quorumPercentage is the quorum percentage to use for the aggregator. +// +// Returns a new signature aggregator instance, or an error if creation fails. +func NewSignatureAggregator( + network models.Network, + logLevel logging.Level, + subnetID ids.ID, + quorumPercentage uint64, + extraPeerEndpoints []info.Peer, +) (*SignatureAggregator, error) { + registerer := prometheus.NewRegistry() + peerNetwork, err := createAppRequestNetwork(network, logLevel, registerer, extraPeerEndpoints) + if err != nil { + return nil, err + } + logger := logging.NewLogger( + "init-aggregator", + logging.NewWrappedCore( + logLevel, + os.Stdout, + logging.JSON.ConsoleEncoder(), + ), + ) + return initSignatureAggregator(peerNetwork, logger, registerer, subnetID, quorumPercentage) +} + +// AggregateSignatures aggregates signatures for a given message and justification. +// +// msg is the Hex encoded message to be signed +// justification is the hex encoded justification for the signature. +// Returns the signed message as a hexadecimal string, and an error if the operation fails. +func (s *SignatureAggregator) AggregateSignatures( + msg string, + justification string, +) (string, error) { + // prepare message + decodedMessage, err := hex.DecodeString( + awmUtils.SanitizeHexString(msg), + ) + if err != nil { + return "", fmt.Errorf("failed to decode message: %w", err) + } + message, err := awmTypes.UnpackWarpMessage(decodedMessage) + if err != nil { + return "", fmt.Errorf("failed to unpack warp message: %w", err) + } + // prepare justification + justificationBytes, err := hex.DecodeString( + awmUtils.SanitizeHexString(justification), + ) + if err != nil { + return "", fmt.Errorf("failed to decode justification: %w", err) + } + // checks + if awmUtils.IsEmptyOrZeroes(message.Bytes()) && awmUtils.IsEmptyOrZeroes(justificationBytes) { + return "", fmt.Errorf("message and justification cannot be empty") + } + + // aggregate signatures + signedMessage, err := s.Sign( + message, + justificationBytes, + ) + return hex.EncodeToString(signedMessage.Bytes()), err +} + +// Sign aggregates signatures for a given message and justification. +// +// msg is the message to be signed +// justification is the justification for the signature. +// Returns the signed message, and an error if the operation fails. +func (s *SignatureAggregator) Sign( + msg *warp.UnsignedMessage, + justification []byte, +) (*warp.Message, error) { + return s.aggregator.CreateSignedMessage( + msg, + justification, + s.subnetID, + s.quorumPercentage, + ) +} diff --git a/sdk/interchain/signature-aggregator_test.go b/sdk/interchain/signature-aggregator_test.go new file mode 100644 index 000000000..a7ce6b219 --- /dev/null +++ b/sdk/interchain/signature-aggregator_test.go @@ -0,0 +1,47 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved +// See the file LICENSE for licensing terms. +package interchain + +import ( + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/awm-relayer/peers/mocks" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +var subnetID ids.ID + +func instantiateAggregator(t *testing.T) ( + *SignatureAggregator, + *mocks.MockAppRequestNetwork, + error, +) { + mockNetwork := mocks.NewMockAppRequestNetwork(gomock.NewController(t)) + subnetID = ids.GenerateTestID() + aggregator, err := initSignatureAggregator( + mockNetwork, + logging.NoLog{}, + prometheus.DefaultRegisterer, + subnetID, + DefaultQuorumPercentage, + ) + require.Equal(t, err, nil) + return aggregator, mockNetwork, err +} + +func TestSignatureAggregator(t *testing.T) { + sa, _, err := instantiateAggregator(t) + require.Nil(t, err) + // basic checks + require.Equal(t, sa.quorumPercentage, DefaultQuorumPercentage) + require.Equal(t, sa.subnetID, subnetID) + require.NotNil(t, sa.aggregator) + msg, err := warp.NewUnsignedMessage(0, subnetID, []byte{}) + require.Nil(t, err) + require.NotNil(t, msg) +} diff --git a/sdk/validatormanager/root.go b/sdk/validatormanager/root.go new file mode 100644 index 000000000..0c1f21b00 --- /dev/null +++ b/sdk/validatormanager/root.go @@ -0,0 +1,285 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validatormanager + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/sdk/interchain" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + avagoconstants "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" +) + +type ValidatorManagerSettings struct { + SubnetID [32]byte + ChurnPeriodSeconds uint64 + MaximumChurnPercentage uint8 +} + +type NativeTokenValidatorManagerSettings struct { + BaseSettings ValidatorManagerSettings + MinimumStakeAmount *big.Int + MaximumStakeAmount *big.Int + MinimumStakeDuration uint64 + MinimumDelegationFeeBips uint16 + MaximumStakeMultiplier uint8 + WeightToValueFactor *big.Int + RewardCalculator common.Address +} + +const ( + ValidatorContractAddress = "0xC0DEBA5E0000000000000000000000000000000" + ProxyContractAddress = "0xFEEDC0DE0000000000000000000000000000000" + ProxyAdminContractAddress = "0xC0FFEE1234567890aBcDEF1234567890AbCdEf34" + RewardCalculatorAddress = "0xDEADC0DE0000000000000000000000000000000" + + DefaultPoSMinimumStakeAmount = 1 + DefaultPoSMaximumStakeAmount = 1000 + DefaultPoSMinimumStakeDuration = 100 + DefaultPoSDMinimumDelegationFee = 1 + DefaultPoSMaximumStakeMultiplier = 1 + DefaultPoSWeightToValueFactor = 1 +) + +var ( + ErrDelegatorIneligibleForRewards = fmt.Errorf("delegator ineligible for rewards") + ErrInvalidBLSPublicKey = fmt.Errorf("invalid BLS public key") + ErrAlreadyInitialized = fmt.Errorf("the contract is already initialized") + ErrInvalidMaximumChurnPercentage = fmt.Errorf("unvalid churn percentage") + ErrInvalidValidationID = fmt.Errorf("invalid validation id") + ErrInvalidValidatorStatus = fmt.Errorf("invalid validator status") + ErrMaxChurnRateExceeded = fmt.Errorf("max churn rate exceeded") + ErrInvalidInitializationStatus = fmt.Errorf("validators set already initialized") + ErrInvalidValidatorManagerBlockchainID = fmt.Errorf("invalid validator manager blockchain ID") + ErrInvalidValidatorManagerAddress = fmt.Errorf("invalid validator manager address") + ErrNodeAlreadyRegistered = fmt.Errorf("node already registered") + ErrInvalidSubnetConversionID = fmt.Errorf("invalid subnet conversion id") + ErrInvalidRegistrationExpiry = fmt.Errorf("invalid registration expiry") + ErrInvalidBLSKeyLength = fmt.Errorf("invalid BLS key length") + ErrInvalidNodeID = fmt.Errorf("invalid node id") + ErrInvalidWarpMessage = fmt.Errorf("invalid warp message") + ErrInvalidWarpSourceChainID = fmt.Errorf("invalid wapr source chain ID") + ErrInvalidWarpOriginSenderAddress = fmt.Errorf("invalid warp origin sender address") + ErrInvalidCodecID = fmt.Errorf("invalid codec ID") + ErrInvalidConversionID = fmt.Errorf("invalid conversion ID") + ErrInvalidDelegationFee = fmt.Errorf("invalid delegation fee") + ErrInvalidDelegationID = fmt.Errorf("invalid delegation ID") + ErrInvalidDelegatorStatus = fmt.Errorf("invalid delegator status") + ErrInvalidMessageLength = fmt.Errorf("invalid message length") + ErrInvalidMessageType = fmt.Errorf("invalid message type") + ErrInvalidMinStakeDuration = fmt.Errorf("invalid min stake duration") + ErrInvalidNonce = fmt.Errorf("invalid nonce") + ErrInvalidPChainOwnerThreshold = fmt.Errorf("invalid pchain owner threshold") + ErrInvalidStakeAmount = fmt.Errorf("invalid stake amount") + ErrInvalidStakeMultiplier = fmt.Errorf("invalid stake multiplier") + ErrInvalidTokenAddress = fmt.Errorf("invalid token address") + ErrInvalidTotalWeight = fmt.Errorf("invalid total weight") + ErrMaxWeightExceeded = fmt.Errorf("max weight exceeded") + ErrMinStakeDurationNotPassed = fmt.Errorf("min stake duration not passed") + ErrPChainOwnerAddressesNotSorted = fmt.Errorf("pchain owner addresses not sorted") + ErrUnauthorizedOwner = fmt.Errorf("unauthorized owner") + ErrUnexpectedRegistrationStatus = fmt.Errorf("unexpected registration status") + ErrValidatorIneligibleForRewards = fmt.Errorf("validator ineligible for rewards") + ErrValidatorNotPoS = fmt.Errorf("validator not PoS") + ErrZeroWeightToValueFactor = fmt.Errorf("zero weight to value factor") + ErrorSignatureToError = map[string]error{ + "InvalidInitialization()": ErrAlreadyInitialized, + "InvalidMaximumChurnPercentage(uint8)": ErrInvalidMaximumChurnPercentage, + "InvalidValidationID(bytes32)": ErrInvalidValidationID, + "InvalidValidatorStatus(uint8)": ErrInvalidValidatorStatus, + "MaxChurnRateExceeded(uint64)": ErrMaxChurnRateExceeded, + "InvalidInitializationStatus()": ErrInvalidInitializationStatus, + "InvalidValidatorManagerBlockchainID(bytes32)": ErrInvalidValidatorManagerBlockchainID, + "InvalidValidatorManagerAddress(address)": ErrInvalidValidatorManagerAddress, + "NodeAlreadyRegistered(bytes)": ErrNodeAlreadyRegistered, + "InvalidSubnetConversionID(bytes32,bytes32)": ErrInvalidSubnetConversionID, + "InvalidRegistrationExpiry(uint64)": ErrInvalidRegistrationExpiry, + "InvalidBLSKeyLength(uint256)": ErrInvalidBLSKeyLength, + "InvalidNodeID(bytes)": ErrInvalidNodeID, + "InvalidWarpMessage()": ErrInvalidWarpMessage, + "InvalidWarpSourceChainID(bytes32)": ErrInvalidWarpSourceChainID, + "InvalidWarpOriginSenderAddress(address)": ErrInvalidWarpOriginSenderAddress, + "DelegatorIneligibleForRewards(bytes32)": ErrDelegatorIneligibleForRewards, + "InvalidBLSPublicKey()": ErrInvalidBLSPublicKey, + "InvalidCodecID(uint32)": ErrInvalidCodecID, + "InvalidConversionID(bytes32,bytes32)": ErrInvalidConversionID, + "InvalidDelegationFee(uint16)": ErrInvalidDelegationFee, + "InvalidDelegationID(bytes32)": ErrInvalidDelegationID, + "InvalidDelegatorStatus(DelegatorStatus)": ErrInvalidDelegatorStatus, + "InvalidMessageLength(uint32,uint32)": ErrInvalidMessageLength, + "InvalidMessageType()": ErrInvalidMessageType, + "InvalidMinStakeDuration(uint64)": ErrInvalidMinStakeDuration, + "InvalidNonce(uint64)": ErrInvalidNonce, + "InvalidPChainOwnerThreshold(uint256,uint256)": ErrInvalidPChainOwnerThreshold, + "InvalidStakeAmount(uint256)": ErrInvalidStakeAmount, + "InvalidStakeMultiplier(uint8)": ErrInvalidStakeMultiplier, + "InvalidTokenAddress(address)": ErrInvalidTokenAddress, + "InvalidTotalWeight(uint256)": ErrInvalidTotalWeight, + "MaxWeightExceeded(uint64)": ErrMaxWeightExceeded, + "MinStakeDurationNotPassed(uint64)": ErrMinStakeDurationNotPassed, + "PChainOwnerAddressesNotSorted()": ErrPChainOwnerAddressesNotSorted, + "UnauthorizedOwner(address)": ErrUnauthorizedOwner, + "UnexpectedRegistrationStatus(bool)": ErrUnexpectedRegistrationStatus, + "ValidatorIneligibleForRewards(bytes32)": ErrValidatorIneligibleForRewards, + "ValidatorNotPoS(bytes32)": ErrValidatorNotPoS, + "ZeroWeightToValueFactor()": ErrZeroWeightToValueFactor, + } +) + +type PoSParams struct { + MinimumStakeAmount *big.Int + MaximumStakeAmount *big.Int + MinimumStakeDuration uint64 + MinimumDelegationFee uint16 + MaximumStakeMultiplier uint8 + WeightToValueFactor *big.Int + RewardCalculatorAddress string +} + +func (p PoSParams) Verify() error { + if p.MinimumStakeAmount.Cmp(big.NewInt(0)) < 0 { + return fmt.Errorf("minimum stake amount cannot be negative") + } + if p.MaximumStakeAmount.Cmp(big.NewInt(0)) < 0 { + return fmt.Errorf("maximum stake amount cannot be negative") + } + if p.MaximumStakeAmount.Cmp(p.MinimumStakeAmount) < 0 { + return fmt.Errorf("maximum stake amount cannot be less than minimum stake amount") + } + if p.WeightToValueFactor.Cmp(big.NewInt(0)) < 0 { + return fmt.Errorf("weight to value factor cannot be negative") + } + if p.RewardCalculatorAddress == "" { + return fmt.Errorf("reward calculator address cannot be empty") + } + return nil +} + +// GetPChainSubnetConversionWarpMessage constructs p-chain-validated (signed) subnet conversion warp +// message, to be sent to the validators manager when +// initializing validators set +// the message specifies [subnetID] that is being converted +// together with the validator's manager [managerBlockchainID], +// [managerAddress], and the initial list of [validators] +func GetPChainSubnetConversionWarpMessage( + network models.Network, + aggregatorLogLevel logging.Level, + aggregatorQuorumPercentage uint64, + aggregatorExtraPeerEndpoints []info.Peer, + subnetID ids.ID, + managerBlockchainID ids.ID, + managerAddress common.Address, + convertSubnetValidators []*txs.ConvertSubnetToL1Validator, +) (*warp.Message, error) { + validators := []warpMessage.SubnetToL1ConverstionValidatorData{} + for _, convertSubnetValidator := range convertSubnetValidators { + validators = append(validators, warpMessage.SubnetToL1ConverstionValidatorData{ + NodeID: convertSubnetValidator.NodeID[:], + BLSPublicKey: convertSubnetValidator.Signer.PublicKey, + Weight: convertSubnetValidator.Weight, + }) + } + subnetConversionData := warpMessage.SubnetToL1ConversionData{ + SubnetID: subnetID, + ManagerChainID: managerBlockchainID, + ManagerAddress: managerAddress.Bytes(), + Validators: validators, + } + subnetConversionID, err := warpMessage.SubnetToL1ConversionID(subnetConversionData) + if err != nil { + return nil, err + } + addressedCallPayload, err := warpMessage.NewSubnetToL1Conversion(subnetConversionID) + if err != nil { + return nil, err + } + subnetConversionAddressedCall, err := warpPayload.NewAddressedCall( + nil, + addressedCallPayload.Bytes(), + ) + if err != nil { + return nil, err + } + subnetConversionUnsignedMessage, err := warp.NewUnsignedMessage( + network.ID, + avagoconstants.PlatformChainID, + subnetConversionAddressedCall.Bytes(), + ) + if err != nil { + return nil, err + } + signatureAggregator, err := interchain.NewSignatureAggregator( + network, + aggregatorLogLevel, + subnetID, + aggregatorQuorumPercentage, + aggregatorExtraPeerEndpoints, + ) + if err != nil { + return nil, err + } + return signatureAggregator.Sign(subnetConversionUnsignedMessage, subnetID[:]) +} + +// InitializeValidatorsSet calls poa manager validators set init method, +// passing to it the p-chain signed [subnetConversionSignedMessage] +// to verify p-chain already processed the associated ConvertSubnetTx +func InitializeValidatorsSet( + rpcURL string, + managerAddress common.Address, + privateKey string, + subnetID ids.ID, + managerBlockchainID ids.ID, + convertSubnetValidators []*txs.ConvertSubnetToL1Validator, + subnetConversionSignedMessage *warp.Message, +) (*types.Transaction, *types.Receipt, error) { + type InitialValidator struct { + NodeID []byte + BlsPublicKey []byte + Weight uint64 + } + type SubnetConversionData struct { + SubnetID [32]byte + ValidatorManagerBlockchainID [32]byte + ValidatorManagerAddress common.Address + InitialValidators []InitialValidator + } + validators := []InitialValidator{} + for _, convertSubnetValidator := range convertSubnetValidators { + validators = append(validators, InitialValidator{ + NodeID: convertSubnetValidator.NodeID[:], + BlsPublicKey: convertSubnetValidator.Signer.PublicKey[:], + Weight: convertSubnetValidator.Weight, + }) + } + subnetConversionData := SubnetConversionData{ + SubnetID: subnetID, + ValidatorManagerBlockchainID: managerBlockchainID, + ValidatorManagerAddress: managerAddress, + InitialValidators: validators, + } + return contract.TxToMethodWithWarpMessage( + rpcURL, + privateKey, + managerAddress, + subnetConversionSignedMessage, + big.NewInt(0), + "initialize validator set", + ErrorSignatureToError, + "initializeValidatorSet((bytes32,bytes32,address,[(bytes,bytes,uint64)]),uint32)", + subnetConversionData, + uint32(0), + ) +} diff --git a/sdk/validatormanager/validator_manager_poa.go b/sdk/validatormanager/validator_manager_poa.go new file mode 100644 index 000000000..72e89db51 --- /dev/null +++ b/sdk/validatormanager/validator_manager_poa.go @@ -0,0 +1,44 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validatormanager + +import ( + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanchego/ids" +) + +// PoAValidatorManagerInitialize initializes contract [managerAddress] at [rpcURL], to +// manage validators on [subnetID], with +// owner given by [ownerAddress] +func PoAValidatorManagerInitialize( + rpcURL string, + managerAddress common.Address, + privateKey string, + subnetID ids.ID, + ownerAddress common.Address, +) (*types.Transaction, *types.Receipt, error) { + const ( + defaultChurnPeriodSeconds = uint64(0) + defaultMaximumChurnPercentage = uint8(20) + ) + params := ValidatorManagerSettings{ + SubnetID: subnetID, + ChurnPeriodSeconds: defaultChurnPeriodSeconds, + MaximumChurnPercentage: defaultMaximumChurnPercentage, + } + return contract.TxToMethod( + rpcURL, + privateKey, + managerAddress, + nil, + "initialize PoA manager", + ErrorSignatureToError, + "initialize((bytes32,uint64,uint8),address)", + params, + ownerAddress, + ) +} diff --git a/sdk/validatormanager/validator_manager_pos.go b/sdk/validatormanager/validator_manager_pos.go new file mode 100644 index 000000000..817accc8a --- /dev/null +++ b/sdk/validatormanager/validator_manager_pos.go @@ -0,0 +1,56 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validatormanager + +import ( + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" +) + +// initializes contract [managerAddress] at [rpcURL], to +// manage validators on [subnetID] using PoS specific settings +func PoSValidatorManagerInitialize( + rpcURL string, + managerAddress common.Address, + privateKey string, + subnetID [32]byte, + posParams PoSParams, +) (*types.Transaction, *types.Receipt, error) { + if err := posParams.Verify(); err != nil { + return nil, nil, err + } + var ( + defaultChurnPeriodSeconds = uint64(0) // no churn period + defaultMaximumChurnPercentage = uint8(20) // 20% of the validator set can be churned per churn period + ) + + baseSettings := ValidatorManagerSettings{ + SubnetID: subnetID, + ChurnPeriodSeconds: defaultChurnPeriodSeconds, + MaximumChurnPercentage: defaultMaximumChurnPercentage, + } + + params := NativeTokenValidatorManagerSettings{ + BaseSettings: baseSettings, + MinimumStakeAmount: posParams.MinimumStakeAmount, + MaximumStakeAmount: posParams.MaximumStakeAmount, + MinimumStakeDuration: posParams.MinimumStakeDuration, + MinimumDelegationFeeBips: posParams.MinimumDelegationFee, + MaximumStakeMultiplier: posParams.MaximumStakeMultiplier, + WeightToValueFactor: posParams.WeightToValueFactor, + RewardCalculator: common.HexToAddress(posParams.RewardCalculatorAddress), + } + + return contract.TxToMethod( + rpcURL, + privateKey, + managerAddress, + nil, + "initialize Native Token PoS manager", + ErrorSignatureToError, + "initialize(((bytes32,uint64,uint8),uint256,uint256,uint64,uint16,uint8,uint256,address))", + params, + ) +} diff --git a/tests/e2e/assets/test_bootstrap_validator.json b/tests/e2e/assets/test_bootstrap_validator.json new file mode 100644 index 000000000..ec8799573 --- /dev/null +++ b/tests/e2e/assets/test_bootstrap_validator.json @@ -0,0 +1,10 @@ +[ + { + "NodeID": "NodeID-144PM69m93kSFyfTHMwULTmoGZSWzQ4C1", + "Weight": 20, + "Balance": 5, + "BLSPublicKey": "0x80b7851ce335cee149b7cfffbf6cf0bbca3c9b25026a24056e610976d095906e833a66d5ca5c56c23a3fe50e8785a81f", + "BLSProofOfPossession": "0x89e1d6d47ff04ec0c78501a029865140e9ec12baba75a95bfc5710b3fecb8db4b6cecb5ccb1136e19f88db0539deb4420306dd60145024197b41cf89179790f20146fba398bc4d13e08540ea812207f736ca007275e4ebdb840065fdb38573de", + "ChangeOwnerAddr": "P-custom1y5ku603lh583xs9v50p8kk0awcqzgeq0mezkqr" + } +] diff --git a/tests/e2e/commands/cleanup.go b/tests/e2e/commands/cleanup.go index a7c1fb7fd..4f11a5cb5 100644 --- a/tests/e2e/commands/cleanup.go +++ b/tests/e2e/commands/cleanup.go @@ -44,6 +44,9 @@ func DeleteE2ECluster() { homeDir := usr.HomeDir relativePath := "nodes" content, err := os.ReadFile(filepath.Join(homeDir, constants.BaseDirName, relativePath, constants.ClustersConfigFileName)) + if err == os.ErrNotExist { + return + } gomega.Expect(err).Should(gomega.BeNil()) clustersConfig := models.ClustersConfig{} err = json.Unmarshal(content, &clustersConfig) diff --git a/tests/e2e/commands/etna.go b/tests/e2e/commands/etna.go new file mode 100644 index 000000000..3caeb5d34 --- /dev/null +++ b/tests/e2e/commands/etna.go @@ -0,0 +1,327 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package commands + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + "github.com/onsi/gomega" +) + +type SubnetManagementType uint + +const ( + Unknown SubnetManagementType = iota + PoA + PoS +) + +const ( + etnaDevnetFlag = "--etna-devnet" + PoSString = "proof-of-stake" + PoAString = "proof-of-authority" +) + +func CreateEtnaSubnetEvmConfig( + subnetName string, + ewoqEVMAddress string, + subnetManagementType SubnetManagementType, +) { + // Check config does not already exist + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeFalse()) + + rewardBasisPoints := "" + subnetManagementStr := PoAString + if subnetManagementType == PoS { + rewardBasisPoints = "--reward-basis-points=100" + subnetManagementStr = PoSString + } + // Create config + cmd := exec.Command( + CLIBinary, + "blockchain", + "create", + subnetName, + "--evm", + fmt.Sprintf("--%s", subnetManagementStr), + "--validator-manager-owner", + ewoqEVMAddress, + "--proxy-contract-owner", + ewoqEVMAddress, + "--test-defaults", + "--evm-chain-id=99999", + "--evm-token=TOK", + "--"+constants.SkipUpdateFlag, + ) + if rewardBasisPoints != "" { + cmd.Args = append(cmd.Args, rewardBasisPoints) + } + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + + // Config should now exist + exists, err = utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) +} + +func CreateLocalEtnaDevnetNode( + clusterName string, + numNodes int, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "node", + "local", + "start", + clusterName, + etnaDevnetFlag, + "--num-nodes", + fmt.Sprintf("%d", numNodes), + "--"+constants.SkipUpdateFlag, + ) + fmt.Println(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func DestroyLocalNode( + clusterName string, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "node", + "local", + "destroy", + clusterName, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func DeployEtnaSubnetToCluster( + subnetName string, + clusterName string, + bootstrapEndpoints []string, + ewoqPChainAddress string, + convertOnly bool, +) (string, error) { + convertOnlyFlag := "" + if convertOnly { + convertOnlyFlag = "--convert-only" + } + bootstrapEndpointsFlag := "" + if len(bootstrapEndpoints) > 0 { + bootstrapEndpointsFlag = "--bootstrap-endpoints=" + strings.Join(bootstrapEndpoints, ",") + } + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // Deploy subnet on etna devnet with local machine as bootstrap validator + cmd := exec.Command( + CLIBinary, + "blockchain", + "deploy", + subnetName, + "--cluster", + clusterName, + bootstrapEndpointsFlag, + convertOnlyFlag, + "--ewoq", + "--change-owner-address", + ewoqPChainAddress, + "--"+constants.SkipUpdateFlag, + ) + fmt.Println(cmd) + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func TrackLocalEtnaSubnet( + clusterName string, + subnetName string, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "node", + "local", + "track", + clusterName, + subnetName, + "--"+constants.SkipUpdateFlag, + ) + fmt.Println(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func InitValidatorManager( + subnetName string, + clusterName string, + endpoint string, + blockchainID string, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "contract", + "initValidatorManager", + subnetName, + "--cluster", + clusterName, + "--endpoint", + endpoint, + "--rpc", + fmt.Sprintf("%s/ext/bc/%s/rpc", endpoint, blockchainID), + "--genesis-key", + "--"+constants.SkipUpdateFlag, + ) + fmt.Println(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func AddEtnaSubnetValidatorToCluster( + clusterName string, + subnetName string, + nodeEndpoint string, + ewoqPChainAddress string, + balance int, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "blockchain", + "addValidator", + subnetName, + "--cluster", + clusterName, + "--node-endpoint", + nodeEndpoint, + "--ewoq", + "--balance", + strconv.Itoa(balance), + "--remaining-balance-owner", + ewoqPChainAddress, + "--disable-owner", + ewoqPChainAddress, + "--stake-amount", + "2", + "--delegation-fee", + "100", + "--staking-period", + "100s", + "--"+constants.SkipUpdateFlag, + ) + fmt.Println(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func RemoveEtnaSubnetValidatorFromCluster( + clusterName string, + subnetName string, + nodeEndpoint string, + keyName string, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "blockchain", + "removeValidator", + subnetName, + "--cluster", + clusterName, + "--node-endpoint", + nodeEndpoint, + "--blockchain-genesis-key", + "--blockchain-key", + keyName, + "--key", + keyName, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +func GetLocalClusterStatus( + clusterName string, + blockchainName string, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "node", + "local", + "status", + clusterName, + "--"+constants.SkipUpdateFlag, + ) + if blockchainName != "" { + cmd.Args = append(cmd.Args, "--blockchain", blockchainName) + } + fmt.Println(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} diff --git a/tests/e2e/commands/subnet.go b/tests/e2e/commands/subnet.go index ccd8b3b97..a926e32d2 100644 --- a/tests/e2e/commands/subnet.go +++ b/tests/e2e/commands/subnet.go @@ -16,20 +16,36 @@ import ( "github.com/onsi/gomega" ) -const subnetEVMMainnetChainID = 11 +const ( + subnetEVMMainnetChainID = 11 + poaValidatorManagerOwner = "0x2e6FcBb9d4E17eC4cF67eddfa7D32eabC4cdCFc6" + bootstrapFilepathFlag = "--bootstrap-filepath" + avalancheGoPath = "--avalanchego-path" + localNodeClusterName = "testLocalNode" + etnaTestSubnet = "etnaTestSubnet" +) /* #nosec G204 */ -func CreateSubnetEvmConfig(subnetName string, genesisPath string) (string, string) { +func CreateSubnetEvmConfigNonSOV(subnetName string, genesisPath string) (string, string) { + mapper := utils.NewVersionMapper() + mapping, err := utils.GetVersionMapping(mapper) + gomega.Expect(err).Should(gomega.BeNil()) + // let's use a SubnetEVM version which has a guaranteed compatible avago + CreateSubnetEvmConfigWithVersionNonSOV(subnetName, genesisPath, mapping[utils.LatestEVM2AvagoKey]) + return mapping[utils.LatestEVM2AvagoKey], mapping[utils.LatestAvago2EVMKey] +} + +func CreateSubnetEvmConfigSOV(subnetName string, genesisPath string) (string, string) { mapper := utils.NewVersionMapper() mapping, err := utils.GetVersionMapping(mapper) gomega.Expect(err).Should(gomega.BeNil()) // let's use a SubnetEVM version which has a guaranteed compatible avago - CreateSubnetEvmConfigWithVersion(subnetName, genesisPath, mapping[utils.LatestEVM2AvagoKey]) + CreateSubnetEvmConfigWithVersionSOV(subnetName, genesisPath, mapping[utils.LatestEVM2AvagoKey]) return mapping[utils.LatestEVM2AvagoKey], mapping[utils.LatestAvago2EVMKey] } /* #nosec G204 */ -func CreateSubnetEvmConfigWithVersion(subnetName string, genesisPath string, version string) { +func CreateSubnetEvmConfigWithVersionNonSOV(subnetName string, genesisPath string, version string) { // Check config does not already exist exists, err := utils.SubnetConfigExists(subnetName) gomega.Expect(err).Should(gomega.BeNil()) @@ -41,6 +57,7 @@ func CreateSubnetEvmConfigWithVersion(subnetName string, genesisPath string, ver "create", "--genesis", genesisPath, + "--sovereign=false", "--evm", subnetName, "--" + constants.SkipUpdateFlag, @@ -68,6 +85,50 @@ func CreateSubnetEvmConfigWithVersion(subnetName string, genesisPath string, ver gomega.Expect(exists).Should(gomega.BeTrue()) } +func CreateSubnetEvmConfigWithVersionSOV(subnetName string, genesisPath string, version string) { + // Check config does not already exist + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeFalse()) + + // Create config + cmdArgs := []string{ + SubnetCmd, + "create", + "--genesis", + genesisPath, + "--evm", + subnetName, + "--proof-of-authority", + "--validator-manager-owner", + poaValidatorManagerOwner, + "--proxy-contract-owner", + poaValidatorManagerOwner, + "--" + constants.SkipUpdateFlag, + "--teleporter=false", + "--evm-token", + "TOK", + } + if version == "" { + cmdArgs = append(cmdArgs, "--latest") + } else { + cmdArgs = append(cmdArgs, "--vm-version", version) + } + cmd := exec.Command(CLIBinary, cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + + // Config should now exist + exists, err = utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) +} + /* #nosec G204 */ func ConfigureChainConfig(subnetName string, genesisPath string) { // run configure @@ -105,7 +166,7 @@ func ConfigurePerNodeChainConfig(subnetName string, perNodeChainConfigPath strin } /* #nosec G204 */ -func CreateCustomVMConfig(subnetName string, genesisPath string, vmPath string) { +func CreateCustomVMConfigNonSOV(subnetName string, genesisPath string, vmPath string) { // Check config does not already exist exists, err := utils.SubnetConfigExists(subnetName) gomega.Expect(err).Should(gomega.BeNil()) @@ -122,6 +183,62 @@ func CreateCustomVMConfig(subnetName string, genesisPath string, vmPath string) "create", "--genesis", genesisPath, + "--sovereign=false", + "--custom", + subnetName, + "--custom-vm-path", + vmPath, + "--"+constants.SkipUpdateFlag, + "--teleporter=false", + "--evm-token", + "TOK", + ) + output, err := cmd.CombinedOutput() + if err != nil { + var ( + exitErr *exec.ExitError + stderr string + ) + if errors.As(err, &exitErr) { + stderr = string(exitErr.Stderr) + } + fmt.Println(string(output)) + utils.PrintStdErr(err) + fmt.Println(stderr) + gomega.Expect(err).Should(gomega.BeNil()) + } + + // Config should now exist + exists, err = utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + exists, err = utils.SubnetCustomVMExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) +} + +func CreateCustomVMConfigSOV(subnetName string, genesisPath string, vmPath string) { + // Check config does not already exist + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeFalse()) + // Check vm binary does not already exist + exists, err = utils.SubnetCustomVMExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeFalse()) + + // Create config + cmd := exec.Command( + CLIBinary, + SubnetCmd, + "create", + "--genesis", + genesisPath, + "--proof-of-authority", + "--validator-manager-owner", + poaValidatorManagerOwner, + "--proxy-contract-owner", + poaValidatorManagerOwner, "--custom", subnetName, "--custom-vm-path", @@ -180,38 +297,62 @@ func DeleteSubnetConfig(subnetName string) { // Returns the deploy output /* #nosec G204 */ -func DeploySubnetLocally(subnetName string) string { - return DeploySubnetLocallyWithArgs(subnetName, "", "") +func DeploySubnetLocallyNonSOV(subnetName string) string { + return DeploySubnetLocallyWithArgsNonSOV(subnetName, "", "") +} + +func DeploySubnetLocallySOV(subnetName string) string { + return DeploySubnetLocallyWithArgsSOV(subnetName, "", "") } /* #nosec G204 */ -func DeploySubnetLocallyExpectError(subnetName string) { +func DeploySubnetLocallyExpectErrorNonSOV(subnetName string) { + mapper := utils.NewVersionMapper() + mapping, err := utils.GetVersionMapping(mapper) + gomega.Expect(err).Should(gomega.BeNil()) + + DeploySubnetLocallyWithArgsExpectErrorNonSOV(subnetName, mapping[utils.OnlyAvagoKey], "") +} + +func DeploySubnetLocallyExpectErrorSOV(subnetName string) { mapper := utils.NewVersionMapper() mapping, err := utils.GetVersionMapping(mapper) gomega.Expect(err).Should(gomega.BeNil()) - DeploySubnetLocallyWithArgsExpectError(subnetName, mapping[utils.OnlyAvagoKey], "") + DeploySubnetLocallyWithArgsExpectErrorSOV(subnetName, mapping[utils.OnlyAvagoKey], "") } // Returns the deploy output /* #nosec G204 */ -func DeploySubnetLocallyWithViperConf(subnetName string, confPath string) string { +func DeploySubnetLocallyWithViperConfNonSOV(subnetName string, confPath string) string { + mapper := utils.NewVersionMapper() + mapping, err := utils.GetVersionMapping(mapper) + gomega.Expect(err).Should(gomega.BeNil()) + + return DeploySubnetLocallyWithArgsNonSOV(subnetName, mapping[utils.OnlyAvagoKey], confPath) +} + +func DeploySubnetLocallyWithViperConfSOV(subnetName string, confPath string) string { mapper := utils.NewVersionMapper() mapping, err := utils.GetVersionMapping(mapper) gomega.Expect(err).Should(gomega.BeNil()) - return DeploySubnetLocallyWithArgs(subnetName, mapping[utils.OnlyAvagoKey], confPath) + return DeploySubnetLocallyWithArgsSOV(subnetName, mapping[utils.OnlyAvagoKey], confPath) } // Returns the deploy output /* #nosec G204 */ -func DeploySubnetLocallyWithVersion(subnetName string, version string) string { - return DeploySubnetLocallyWithArgs(subnetName, version, "") +func DeploySubnetLocallyWithVersionNonSOV(subnetName string, version string) string { + return DeploySubnetLocallyWithArgsNonSOV(subnetName, version, "") +} + +func DeploySubnetLocallyWithVersionSOV(subnetName string, version string) string { + return DeploySubnetLocallyWithArgsSOV(subnetName, version, "") } // Returns the deploy output /* #nosec G204 */ -func DeploySubnetLocallyWithArgs(subnetName string, version string, confPath string) string { +func DeploySubnetLocallyWithArgsNonSOV(subnetName string, version string, confPath string) string { // Check config exists exists, err := utils.SubnetConfigExists(subnetName) gomega.Expect(err).Should(gomega.BeNil()) @@ -249,7 +390,45 @@ func DeploySubnetLocallyWithArgs(subnetName string, version string, confPath str return string(output) } -func DeploySubnetLocallyWithArgsAndOutput(subnetName string, version string, confPath string) ([]byte, error) { +func DeploySubnetLocallyWithArgsSOV(subnetName string, version string, confPath string) string { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // Deploy subnet locally + cmdArgs := []string{SubnetCmd, "deploy", "--local", subnetName, "--" + constants.SkipUpdateFlag, bootstrapFilepathFlag + "=" + utils.BootstrapValidatorPath} + if version != "" { + cmdArgs = append(cmdArgs, "--avalanchego-version", version) + } + if confPath != "" { + cmdArgs = append(cmdArgs, "--config", confPath) + } + // in case we want to use specific avago for local tests + debugAvalanchegoPath := os.Getenv(constants.E2EDebugAvalanchegoPath) + if debugAvalanchegoPath != "" { + cmdArgs = append(cmdArgs, "--avalanchego-path", debugAvalanchegoPath) + } + cmd := exec.Command(CLIBinary, cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + var ( + exitErr *exec.ExitError + stderr string + ) + if errors.As(err, &exitErr) { + stderr = string(exitErr.Stderr) + } + fmt.Println(string(output)) + utils.PrintStdErr(err) + fmt.Println(stderr) + } + gomega.Expect(err).Should(gomega.BeNil()) + + return string(output) +} + +func DeploySubnetLocallyWithArgsAndOutputNonSOV(subnetName string, version string, confPath string) ([]byte, error) { // Check config exists exists, err := utils.SubnetConfigExists(subnetName) gomega.Expect(err).Should(gomega.BeNil()) @@ -272,15 +451,87 @@ func DeploySubnetLocallyWithArgsAndOutput(subnetName string, version string, con return cmd.CombinedOutput() } +func DeploySubnetLocallyWithArgsAndOutputSOV(subnetName string, version string, confPath string) ([]byte, error) { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // Deploy subnet locally + cmdArgs := []string{SubnetCmd, "deploy", "--local", subnetName, "--" + constants.SkipUpdateFlag, bootstrapFilepathFlag + "=" + utils.BootstrapValidatorPath} + if version != "" { + cmdArgs = append(cmdArgs, "--avalanchego-version", version) + } + if confPath != "" { + cmdArgs = append(cmdArgs, "--config", confPath) + } + // in case we want to use specific avago for local tests + debugAvalanchegoPath := os.Getenv(constants.E2EDebugAvalanchegoPath) + if debugAvalanchegoPath != "" { + cmdArgs = append(cmdArgs, "--avalanchego-path", debugAvalanchegoPath) + } + cmd := exec.Command(CLIBinary, cmdArgs...) + return cmd.CombinedOutput() +} + /* #nosec G204 */ -func DeploySubnetLocallyWithArgsExpectError(subnetName string, version string, confPath string) { - _, err := DeploySubnetLocallyWithArgsAndOutput(subnetName, version, confPath) +func DeploySubnetLocallyWithArgsExpectErrorNonSOV(subnetName string, version string, confPath string) { + _, err := DeploySubnetLocallyWithArgsAndOutputNonSOV(subnetName, version, confPath) + gomega.Expect(err).Should(gomega.HaveOccurred()) +} + +func DeploySubnetLocallyWithArgsExpectErrorSOV(subnetName string, version string, confPath string) { + _, err := DeploySubnetLocallyWithArgsAndOutputSOV(subnetName, version, confPath) gomega.Expect(err).Should(gomega.HaveOccurred()) } // simulates fuji deploy execution path on a local network /* #nosec G204 */ -func SimulateFujiDeploy( +func SimulateFujiDeployNonSOV( + subnetName string, + key string, + controlKeys string, +) string { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // enable simulation of public network execution paths on a local network + err = os.Setenv(constants.SimulatePublicNetwork, "true") + gomega.Expect(err).Should(gomega.BeNil()) + + // Deploy subnet locally + cmd := exec.Command( + CLIBinary, + SubnetCmd, + "deploy", + "--fuji", + "--threshold", + "1", + "--key", + key, + "--control-keys", + controlKeys, + subnetName, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + + // disable simulation of public network execution paths on a local network + err = os.Unsetenv(constants.SimulatePublicNetwork) + gomega.Expect(err).Should(gomega.BeNil()) + + return string(output) +} + +func SimulateFujiDeploySOV( subnetName string, key string, controlKeys string, @@ -300,6 +551,7 @@ func SimulateFujiDeploy( SubnetCmd, "deploy", "--fuji", + bootstrapFilepathFlag+"="+utils.BootstrapValidatorPath, "--threshold", "1", "--key", @@ -326,7 +578,45 @@ func SimulateFujiDeploy( // simulates mainnet deploy execution path on a local network /* #nosec G204 */ -func SimulateMainnetDeploy( +func SimulateMainnetDeployNonSOV( + subnetName string, + mainnetChainID int, + errorIsExpected bool, +) string { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // enable simulation of public network execution paths on a local network + err = os.Setenv(constants.SimulatePublicNetwork, "true") + gomega.Expect(err).Should(gomega.BeNil()) + + if mainnetChainID == 0 { + mainnetChainID = subnetEVMMainnetChainID + } + + // Deploy subnet locally + return utils.ExecCommand( + CLIBinary, + []string{ + SubnetCmd, + "deploy", + "--mainnet", + "--threshold", + "1", + "--same-control-key", + "--mainnet-chain-id", + fmt.Sprint(mainnetChainID), + subnetName, + "--" + constants.SkipUpdateFlag, + }, + true, + errorIsExpected, + ) +} + +func SimulateMainnetDeploySOV( subnetName string, mainnetChainID int, errorIsExpected bool, @@ -350,6 +640,7 @@ func SimulateMainnetDeploy( []string{ SubnetCmd, "deploy", + bootstrapFilepathFlag + "=" + utils.BootstrapValidatorPath, "--mainnet", "--threshold", "1", @@ -366,7 +657,46 @@ func SimulateMainnetDeploy( // simulates multisig mainnet deploy execution path on a local network /* #nosec G204 */ -func SimulateMultisigMainnetDeploy( +func SimulateMultisigMainnetDeployNonSOV( + subnetName string, + subnetControlAddrs []string, + chainCreationAuthAddrs []string, + txPath string, + errorIsExpected bool, +) string { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // enable simulation of public network execution paths on a local network + err = os.Setenv(constants.SimulatePublicNetwork, "true") + gomega.Expect(err).Should(gomega.BeNil()) + + // Multisig deploy for local subnet with possible tx file generation + return utils.ExecCommand( + CLIBinary, + []string{ + SubnetCmd, + "deploy", + "--mainnet", + "--control-keys", + strings.Join(subnetControlAddrs, ","), + "--subnet-auth-keys", + strings.Join(chainCreationAuthAddrs, ","), + "--output-tx-path", + txPath, + "--mainnet-chain-id", + fmt.Sprint(subnetEVMMainnetChainID), + subnetName, + "--" + constants.SkipUpdateFlag, + }, + true, + errorIsExpected, + ) +} + +func SimulateMultisigMainnetDeploySOV( subnetName string, subnetControlAddrs []string, chainCreationAuthAddrs []string, @@ -389,6 +719,7 @@ func SimulateMultisigMainnetDeploy( SubnetCmd, "deploy", "--mainnet", + bootstrapFilepathFlag + "=" + utils.BootstrapValidatorPath, "--control-keys", strings.Join(subnetControlAddrs, ","), "--subnet-auth-keys", @@ -486,7 +817,7 @@ func SimulateFujiAddValidator( "--fuji", "--key", key, - "--nodeID", + "--node-id", nodeID, "--start-time", start, @@ -534,7 +865,7 @@ func SimulateFujiRemoveValidator( "--fuji", "--key", key, - "--nodeID", + "--node-id", nodeID, subnetName, ) @@ -577,7 +908,7 @@ func SimulateMainnetAddValidator( SubnetCmd, "addValidator", "--mainnet", - "--nodeID", + "--node-id", nodeID, "--start-time", start, @@ -619,7 +950,7 @@ func SimulateFujiJoin( avalanchegoConfig, "--plugin-dir", pluginDir, - "--nodeID", + "--node-id", nodeID, "--force-write", subnetName, @@ -665,7 +996,7 @@ func SimulateMainnetJoin( avalanchegoConfig, "--plugin-dir", pluginDir, - "--nodeID", + "--node-id", nodeID, "--force-write", subnetName, diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index c0cbed1e3..c8e264d9e 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -19,9 +19,16 @@ import ( _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/packageman" _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/root" _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet" - _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/local" - _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/public" - _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/upgrade" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/non-sov/local" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/non-sov/public" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoA" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoS" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/sov/etna" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/sov/local" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/sov/public" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/upgrade/non-sov" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/upgrade/sov" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/validatormanager" ginkgo "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "github.com/onsi/gomega/format" diff --git a/tests/e2e/testcases/errhandling/suite.go b/tests/e2e/testcases/errhandling/suite.go index 014bcdcca..7aaf4db30 100644 --- a/tests/e2e/testcases/errhandling/suite.go +++ b/tests/e2e/testcases/errhandling/suite.go @@ -35,13 +35,24 @@ var _ = ginkgo.Describe("[Error handling]", func() { // delete custom vm utils.DeleteCustomBinary(subnetName) }) - ginkgo.It("subnet-evm has error but booted", func() { + ginkgo.It("subnet-evm has error but booted non SOV", func() { // tip: if you really want to run this, reduce the RequestTimeout ginkgo.Skip("run this manually only, times out") // this will boot the subnet with a bad genesis: // the root gas limit is smaller than the fee config gas limit, should fail - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisBadPath) - out, err := commands.DeploySubnetLocallyWithArgsAndOutput(subnetName, "", "") + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisBadPath) + out, err := commands.DeploySubnetLocallyWithArgsAndOutputNonSOV(subnetName, "", "") + gomega.Expect(err).Should(gomega.HaveOccurred()) + gomega.Expect(out).Should(gomega.ContainSubstring("does not match gas limit")) + fmt.Println(string(out)) + }) + ginkgo.It("subnet-evm has error but booted SOV", func() { + // tip: if you really want to run this, reduce the RequestTimeout + ginkgo.Skip("run this manually only, times out") + // this will boot the subnet with a bad genesis: + // the root gas limit is smaller than the fee config gas limit, should fail + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisBadPath) + out, err := commands.DeploySubnetLocallyWithArgsAndOutputSOV(subnetName, "", "") gomega.Expect(err).Should(gomega.HaveOccurred()) gomega.Expect(out).Should(gomega.ContainSubstring("does not match gas limit")) fmt.Println(string(out)) diff --git a/tests/e2e/testcases/key/suite.go b/tests/e2e/testcases/key/suite.go index 4b954336f..a95b78006 100644 --- a/tests/e2e/testcases/key/suite.go +++ b/tests/e2e/testcases/key/suite.go @@ -134,7 +134,7 @@ var _ = ginkgo.Describe("[Key]", func() { regex1 := `.*NAME.*SUBNET.*ADDRESS.*NETWORK` regex2 := `.*e2eKey.*C-Chain.*0x[a-fA-F0-9]{40}` regex3 := `.*P-Chain.*[(P-custom)(P-fuji)][a-zA-Z0-9]{39}` - regex4 := `.*P-fuji[a-zA-Z0-9]{39}` + regex4 := `.*P-custom[a-zA-Z0-9]{39}` // Create a key output, err := commands.CreateKey(keyName) @@ -145,7 +145,7 @@ var _ = ginkgo.Describe("[Key]", func() { gomega.Expect(err).Should(gomega.BeNil()) // Call list cmd - output, err = commands.ListKeys("fuji", false, false) + output, err = commands.ListKeys("local", false, false) if err != nil { fmt.Println(output) utils.PrintStdErr(err) diff --git a/tests/e2e/testcases/network/suite.go b/tests/e2e/testcases/network/suite.go index ab39d7a2e..91daba3b9 100644 --- a/tests/e2e/testcases/network/suite.go +++ b/tests/e2e/testcases/network/suite.go @@ -23,9 +23,9 @@ var _ = ginkgo.Describe("[Network]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) }) - ginkgo.It("can stop and restart a deployed subnet", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocally(subnetName) + ginkgo.It("can stop and restart a deployed subnet non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -58,6 +58,7 @@ var _ = ginkgo.Describe("[Network]", ginkgo.Ordered, func() { commands.StopNetwork() restartOutput := commands.StartNetwork() rpcs, err = utils.ParseRPCsFromOutput(restartOutput) + fmt.Println(restartOutput) if err != nil { fmt.Println(restartOutput) } @@ -79,9 +80,92 @@ var _ = ginkgo.Describe("[Network]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("clean hard deletes plugin binaries", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocally(subnetName) + ginkgo.It("can stop and restart a deployed subnet SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + // Deploy greeter contract + scriptOutput, scriptErr, err := utils.RunHardhatScript(utils.GreeterScript) + if scriptErr != "" { + fmt.Println(scriptOutput) + fmt.Println(scriptErr) + } + gomega.Expect(err).Should(gomega.BeNil()) + err = utils.ParseGreeterAddress(scriptOutput) + gomega.Expect(err).Should(gomega.BeNil()) + + // Check greeter script before stopping + scriptOutput, scriptErr, err = utils.RunHardhatScript(utils.GreeterCheck) + if scriptErr != "" { + fmt.Println(scriptOutput) + fmt.Println(scriptErr) + } + gomega.Expect(err).Should(gomega.BeNil()) + + commands.StopNetwork() + restartOutput := commands.StartNetwork() + rpcs, err = utils.ParseRPCsFromOutput(restartOutput) + if err != nil { + fmt.Println(restartOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc = rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + // Check greeter contract has right value + scriptOutput, scriptErr, err = utils.RunHardhatScript(utils.GreeterCheck) + if scriptErr != "" { + fmt.Println(scriptOutput) + fmt.Println(scriptErr) + } + gomega.Expect(err).Should(gomega.BeNil()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("clean hard deletes plugin binaries non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + + // check that plugin binaries exist + plugins, err := utils.GetPluginBinaries() + // should have only subnet-evm binary + gomega.Expect(len(plugins)).Should(gomega.Equal(1)) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.CleanNetwork() + + // check that plugin binaries exist + plugins, err = utils.GetPluginBinaries() + // should be empty + gomega.Expect(len(plugins)).Should(gomega.Equal(0)) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("clean hard deletes plugin binaries SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) diff --git a/tests/e2e/testcases/packageman/suite.go b/tests/e2e/testcases/packageman/suite.go index cdb302911..3d2e2d572 100644 --- a/tests/e2e/testcases/packageman/suite.go +++ b/tests/e2e/testcases/packageman/suite.go @@ -40,13 +40,13 @@ var _ = ginkgo.Describe("[Package Management]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) }) - ginkgo.It("can deploy a subnet with subnet-evm version", func() { + ginkgo.It("can deploy a subnet with subnet-evm version non SOV", func() { // check subnet-evm install precondition gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey1])).Should(gomega.BeFalse()) gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.SoloAvagoKey])).Should(gomega.BeFalse()) - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) - deployOutput := commands.DeploySubnetLocallyWithVersion(subnetName, binaryToVersion[utils.SoloAvagoKey]) + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) + deployOutput := commands.DeploySubnetLocallyWithVersionNonSOV(subnetName, binaryToVersion[utils.SoloAvagoKey]) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -68,15 +68,87 @@ var _ = ginkgo.Describe("[Package Management]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can deploy multiple subnet-evm versions", func() { + ginkgo.It("can deploy a subnet with subnet-evm version SOV", func() { + // check subnet-evm install precondition + gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey1])).Should(gomega.BeFalse()) + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.SoloAvagoKey])).Should(gomega.BeFalse()) + + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) + deployOutput := commands.DeploySubnetLocallyWithVersionSOV(subnetName, binaryToVersion[utils.SoloAvagoKey]) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + // check subnet-evm install + gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey1])).Should(gomega.BeTrue()) + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.SoloAvagoKey])).Should(gomega.BeTrue()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can deploy multiple subnet-evm versions non SOV", func() { + // check subnet-evm install precondition + gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey1])).Should(gomega.BeFalse()) + gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey2])).Should(gomega.BeFalse()) + + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) + commands.CreateSubnetEvmConfigWithVersionNonSOV(secondSubnetName, utils.SubnetEvmGenesis2Path, binaryToVersion[utils.SoloSubnetEVMKey2]) + + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs1, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs1).Should(gomega.HaveLen(1)) + + deployOutput = commands.DeploySubnetLocallyNonSOV(secondSubnetName) + rpcs2, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs2).Should(gomega.HaveLen(1)) + + err = utils.SetHardhatRPC(rpcs1[0]) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.SetHardhatRPC(rpcs2[0]) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + // check subnet-evm install + gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey1])).Should(gomega.BeTrue()) + gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey2])).Should(gomega.BeTrue()) + + commands.DeleteSubnetConfig(subnetName) + commands.DeleteSubnetConfig(secondSubnetName) + }) + + ginkgo.It("can deploy multiple subnet-evm versions SOV", func() { // check subnet-evm install precondition gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey1])).Should(gomega.BeFalse()) gomega.Expect(utils.CheckSubnetEVMExists(binaryToVersion[utils.SoloSubnetEVMKey2])).Should(gomega.BeFalse()) - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) - commands.CreateSubnetEvmConfigWithVersion(secondSubnetName, utils.SubnetEvmGenesis2Path, binaryToVersion[utils.SoloSubnetEVMKey2]) + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) + commands.CreateSubnetEvmConfigWithVersionSOV(secondSubnetName, utils.SubnetEvmGenesis2Path, binaryToVersion[utils.SoloSubnetEVMKey2]) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs1, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -84,7 +156,7 @@ var _ = ginkgo.Describe("[Package Management]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(rpcs1).Should(gomega.HaveLen(1)) - deployOutput = commands.DeploySubnetLocally(secondSubnetName) + deployOutput = commands.DeploySubnetLocallySOV(secondSubnetName) rpcs2, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -112,14 +184,74 @@ var _ = ginkgo.Describe("[Package Management]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(secondSubnetName) }) - ginkgo.It("can deploy with multiple avalanchego versions", func() { + ginkgo.It("can deploy with multiple avalanchego versions non SOV", func() { + ginkgo.Skip("skipped until two consecutive avago version with dynamic fees are available") + // check avago install precondition + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago1Key])).Should(gomega.BeFalse()) + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago2Key])).Should(gomega.BeFalse()) + + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.MultiAvagoSubnetEVMKey]) + deployOutput := commands.DeploySubnetLocallyWithVersionNonSOV(subnetName, binaryToVersion[utils.MultiAvago1Key]) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + // Deploy greeter contract + scriptOutput, scriptErr, err := utils.RunHardhatScript(utils.GreeterScript) + if scriptErr != "" { + fmt.Println(scriptOutput) + fmt.Println(scriptErr) + } + gomega.Expect(err).Should(gomega.BeNil()) + err = utils.ParseGreeterAddress(scriptOutput) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + // check avago install + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago1Key])).Should(gomega.BeTrue()) + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago2Key])).Should(gomega.BeFalse()) + + commands.CleanNetwork() + + deployOutput = commands.DeploySubnetLocallyWithVersionNonSOV(subnetName, binaryToVersion[utils.MultiAvago2Key]) + rpcs, err = utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc = rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + // check avago install + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago1Key])).Should(gomega.BeTrue()) + gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago2Key])).Should(gomega.BeTrue()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can deploy with multiple avalanchego versions SOV", func() { ginkgo.Skip("skipped until two consecutive avago version with dynamic fees are available") // check avago install precondition gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago1Key])).Should(gomega.BeFalse()) gomega.Expect(utils.CheckAvalancheGoExists(binaryToVersion[utils.MultiAvago2Key])).Should(gomega.BeFalse()) - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.MultiAvagoSubnetEVMKey]) - deployOutput := commands.DeploySubnetLocallyWithVersion(subnetName, binaryToVersion[utils.MultiAvago1Key]) + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.MultiAvagoSubnetEVMKey]) + deployOutput := commands.DeploySubnetLocallyWithVersionSOV(subnetName, binaryToVersion[utils.MultiAvago1Key]) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -150,7 +282,7 @@ var _ = ginkgo.Describe("[Package Management]", ginkgo.Ordered, func() { commands.CleanNetwork() - deployOutput = commands.DeploySubnetLocallyWithVersion(subnetName, binaryToVersion[utils.MultiAvago2Key]) + deployOutput = commands.DeploySubnetLocallyWithVersionSOV(subnetName, binaryToVersion[utils.MultiAvago2Key]) rpcs, err = utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) diff --git a/tests/e2e/testcases/subnet/non-sov/local/suite.go b/tests/e2e/testcases/subnet/non-sov/local/suite.go new file mode 100644 index 000000000..7d870eae5 --- /dev/null +++ b/tests/e2e/testcases/subnet/non-sov/local/suite.go @@ -0,0 +1,366 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package subnet + +import ( + "context" + "fmt" + "net/url" + "os" + "path" + "strconv" + "strings" + + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + "github.com/ava-labs/avalanche-network-runner/api" + "github.com/ethereum/go-ethereum/common" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + subnetName = "e2eSubnetTest" + secondSubnetName = "e2eSecondSubnetTest" + confPath = "tests/e2e/assets/test_avalanche-cli.json" +) + +var ( + mapping map[string]string + err error +) + +var _ = ginkgo.Describe("[Local Subnet non SOV]", ginkgo.Ordered, func() { + _ = ginkgo.BeforeAll(func() { + mapper := utils.NewVersionMapper() + mapping, err = utils.GetVersionMapping(mapper) + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.AfterEach(func() { + commands.CleanNetwork() + err := utils.DeleteConfigs(subnetName) + if err != nil { + fmt.Println("Clean network error:", err) + } + gomega.Expect(err).Should(gomega.BeNil()) + err = utils.DeleteConfigs(secondSubnetName) + if err != nil { + fmt.Println("Delete config error:", err) + } + gomega.Expect(err).Should(gomega.BeNil()) + + // delete custom vm + utils.DeleteCustomBinary(subnetName) + utils.DeleteCustomBinary(secondSubnetName) + }) + + ginkgo.It("can deploy a custom vm subnet to local non SOV", func() { + customVMPath, err := utils.DownloadCustomVMBin(mapping[utils.SoloSubnetEVMKey1]) + gomega.Expect(err).Should(gomega.BeNil()) + commands.CreateCustomVMConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath, customVMPath) + deployOutput := commands.DeploySubnetLocallyWithVersionNonSOV(subnetName, mapping[utils.SoloAvagoKey]) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can deploy a SubnetEvm subnet to local non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can load viper config and setup node properties for local deploy non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallyWithViperConfNonSOV(subnetName, confPath) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + gomega.Expect(rpc).Should(gomega.HavePrefix("http://127.0.0.1:")) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can't deploy the same subnet twice to local non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + fmt.Println(deployOutput) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + + out, err := commands.DeploySubnetLocallyWithArgsAndOutputNonSOV(subnetName, "", "") + gomega.Expect(err).Should(gomega.HaveOccurred()) + deployOutput = string(out) + rpcs, err = utils.ParseRPCsFromOutput(deployOutput) + if err == nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.HaveOccurred()) + gomega.Expect(rpcs).Should(gomega.HaveLen(0)) + gomega.Expect(deployOutput).Should(gomega.ContainSubstring("has already been deployed")) + }) + + ginkgo.It("can deploy multiple subnets to local non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + commands.CreateSubnetEvmConfigNonSOV(secondSubnetName, utils.SubnetEvmGenesis2Path) + + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs1, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs1).Should(gomega.HaveLen(1)) + + deployOutput = commands.DeploySubnetLocallyNonSOV(secondSubnetName) + rpcs2, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs2).Should(gomega.HaveLen(1)) + + err = utils.SetHardhatRPC(rpcs1[0]) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.SetHardhatRPC(rpcs2[0]) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.DeleteSubnetConfig(subnetName) + commands.DeleteSubnetConfig(secondSubnetName) + }) + + ginkgo.It("can deploy custom chain config non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmAllowFeeRecpPath) + + addr := "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + + chainConfig := "{\"feeRecipient\": \"" + addr + "\"}" + + // create a chain config in tmp + file, err := os.CreateTemp("", constants.ChainConfigFileName+"*") + gomega.Expect(err).Should(gomega.BeNil()) + err = os.WriteFile(file.Name(), []byte(chainConfig), constants.DefaultPerms755) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.ConfigureChainConfig(subnetName, file.Name()) + + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + + rpc := rpcs[0] + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + url, err := url.Parse(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + port, err := strconv.Atoi(url.Port()) + gomega.Expect(err).Should(gomega.BeNil()) + cClient := api.NewEthClient(url.Hostname(), uint(port)) + + ethAddr := common.HexToAddress(addr) + balance, err := cClient.BalanceAt(context.Background(), ethAddr, nil) + gomega.Expect(err).Should(gomega.BeNil()) + + gomega.Expect(balance.Int64()).Should(gomega.Not(gomega.BeZero())) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can deploy with custom per chain config node non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + // create per node chain config + nodesRPCTxFeeCap := map[string]string{ + "node1": "101", + "node2": "102", + "node3": "103", + "node4": "104", + "node5": "105", + } + perNodeChainConfig := "{\n" + i := 0 + for nodeName, rpcTxFeeCap := range nodesRPCTxFeeCap { + commaStr := "," + if i == len(nodesRPCTxFeeCap)-1 { + commaStr = "" + } + perNodeChainConfig += fmt.Sprintf(" \"%s\": {\"rpc-tx-fee-cap\": %s}%s\n", nodeName, rpcTxFeeCap, commaStr) + i++ + } + perNodeChainConfig += "}\n" + + // configure the subnet + file, err := os.CreateTemp("", constants.PerNodeChainConfigFileName+"*") + gomega.Expect(err).Should(gomega.BeNil()) + err = os.WriteFile(file.Name(), []byte(perNodeChainConfig), constants.DefaultPerms755) + gomega.Expect(err).Should(gomega.BeNil()) + commands.ConfigurePerNodeChainConfig(subnetName, file.Name()) + + // deploy + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + + // get blockchain ID + rpcParts := strings.Split(rpcs[0], "/") + gomega.Expect(rpcParts).Should(gomega.HaveLen(7)) + blockchainID := rpcParts[5] + + // verify that plugin logs reflect per node configuration + nodesInfo, err := utils.GetNodesInfo() + gomega.Expect(err).Should(gomega.BeNil()) + for nodeName, nodeInfo := range nodesInfo { + logFile := path.Join(nodeInfo.LogDir, blockchainID+".log") + fileBytes, err := os.ReadFile(logFile) + gomega.Expect(err).Should(gomega.BeNil()) + rpcTxFeeCap, ok := nodesRPCTxFeeCap[nodeName] + gomega.Expect(ok).Should(gomega.BeTrue()) + gomega.Expect(fileBytes).Should(gomega.ContainSubstring("RPCTxFeeCap:%s", rpcTxFeeCap)) + } + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can list a subnet's validators non SOV", func() { + nodeIDs := []string{ + "NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5", + "NodeID-GWPcbFJZFfZreETSoWjPimr846mXEKCtu", + "NodeID-NFBbbJ4qCmNaCzeW7sxErhvWqvEQMnYcN", + "NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ", + "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg", + } + + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + _, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + + output, err := commands.ListValidators(subnetName, "local") + gomega.Expect(err).Should(gomega.BeNil()) + + for _, nodeID := range nodeIDs { + gomega.Expect(output).Should(gomega.ContainSubstring(nodeID)) + } + + commands.DeleteSubnetConfig(subnetName) + }) +}) + +var _ = ginkgo.Describe("[Subnet Compatibility]", func() { + ginkgo.AfterEach(func() { + commands.CleanNetwork() + if err := utils.DeleteConfigs(subnetName); err != nil { + fmt.Println("Clean network error:", err) + gomega.Expect(err).Should(gomega.BeNil()) + } + + if err := utils.DeleteConfigs(secondSubnetName); err != nil { + fmt.Println("Delete config error:", err) + gomega.Expect(err).Should(gomega.BeNil()) + } + }) + + ginkgo.It("can deploy a subnet-evm with old version", func() { + subnetEVMVersion := "v0.6.9" + + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + rpc := rpcs[0] + + err = utils.SetHardhatRPC(rpc) + gomega.Expect(err).Should(gomega.BeNil()) + + err = utils.RunHardhatTests(utils.BaseTest) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can't deploy conflicting vm versions", func() { + // TODO: These shouldn't be hardcoded either + subnetEVMVersion1 := "v0.6.9" + subnetEVMVersion2 := "v0.6.8" + + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) + commands.CreateSubnetEvmConfigWithVersionNonSOV(secondSubnetName, utils.SubnetEvmGenesis2Path, subnetEVMVersion2) + + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(rpcs).Should(gomega.HaveLen(1)) + + commands.DeploySubnetLocallyExpectErrorNonSOV(secondSubnetName) + + commands.DeleteSubnetConfig(subnetName) + commands.DeleteSubnetConfig(secondSubnetName) + }) +}) diff --git a/tests/e2e/testcases/subnet/public/suite.go b/tests/e2e/testcases/subnet/non-sov/public/suite.go similarity index 95% rename from tests/e2e/testcases/subnet/public/suite.go rename to tests/e2e/testcases/subnet/non-sov/public/suite.go index a1f829cf9..1dd6902e6 100644 --- a/tests/e2e/testcases/subnet/public/suite.go +++ b/tests/e2e/testcases/subnet/non-sov/public/suite.go @@ -36,10 +36,10 @@ const ( mainnetChainID = 123456 ) -func deploySubnetToFuji() (string, map[string]utils.NodeInfo) { +func deploySubnetToFujiNonSOV() (string, map[string]utils.NodeInfo) { // deploy - s := commands.SimulateFujiDeploy(subnetName, keyName, controlKeys) - subnetID, err := utils.ParsePublicDeployOutput(s) + s := commands.SimulateFujiDeployNonSOV(subnetName, keyName, controlKeys) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() @@ -69,7 +69,7 @@ func deploySubnetToFuji() (string, map[string]utils.NodeInfo) { return subnetID, nodeInfos } -var _ = ginkgo.Describe("[Public Subnet]", func() { +var _ = ginkgo.Describe("[Public Subnet non SOV]", func() { ginkgo.BeforeEach(func() { // key _ = utils.DeleteKey(keyName) @@ -81,7 +81,7 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { gomega.Expect(err).Should(gomega.BeNil()) // subnet config _ = utils.DeleteConfigs(subnetName) - _, avagoVersion := commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + _, avagoVersion := commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) // local network commands.StartNetworkWithVersion(avagoVersion) @@ -95,7 +95,7 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { }) ginkgo.It("deploy subnet to fuji", func() { - deploySubnetToFuji() + deploySubnetToFujiNonSOV() }) ginkgo.It("deploy subnet to mainnet", func() { @@ -109,9 +109,9 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { gomega.Expect(err).Should(gomega.BeNil()) fmt.Println() fmt.Println(logging.LightRed.Wrap("DEPLOYING SUBNET. VERIFY LEDGER ADDRESS HAS CUSTOM HRP BEFORE SIGNING")) - s := commands.SimulateMainnetDeploy(subnetName, 0, false) + s := commands.SimulateMainnetDeployNonSOV(subnetName, 0, false) // deploy - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() @@ -162,14 +162,14 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { subnetMainnetChainID, err := utils.GetSubnetEVMMainneChainID(subnetName) gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(subnetMainnetChainID).Should(gomega.Equal(uint(0))) - _ = commands.SimulateMainnetDeploy(subnetName, mainnetChainID, true) + _ = commands.SimulateMainnetDeployNonSOV(subnetName, mainnetChainID, true) subnetMainnetChainID, err = utils.GetSubnetEVMMainneChainID(subnetName) gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(subnetMainnetChainID).Should(gomega.Equal(uint(mainnetChainID))) }) ginkgo.It("remove validator fuji", func() { - subnetIDStr, nodeInfos := deploySubnetToFuji() + subnetIDStr, nodeInfos := deploySubnetToFujiNonSOV() // pick a validator to remove var validatorToRemove string @@ -249,7 +249,7 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { // multisig deploy from unfunded ledger1 should not create any subnet/blockchain gomega.Expect(err).Should(gomega.BeNil()) - s := commands.SimulateMultisigMainnetDeploy( + s := commands.SimulateMultisigMainnetDeployNonSOV( subnetName, []string{ledger2Addr, ledger3Addr, ledger4Addr}, []string{ledger2Addr, ledger3Addr}, @@ -269,7 +269,7 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { // multisig deploy from funded ledger1 should create the subnet but not deploy the blockchain, // instead signing only its tx fee as it is not a subnet auth key, // and creating the tx file to wait for subnet auths from ledger2 and ledger3 - s = commands.SimulateMultisigMainnetDeploy( + s = commands.SimulateMultisigMainnetDeployNonSOV( subnetName, []string{ledger2Addr, ledger3Addr, ledger4Addr}, []string{ledger2Addr, ledger3Addr}, @@ -411,7 +411,7 @@ var _ = ginkgo.Describe("[Public Subnet]", func() { txPath, true, ) - toMatch = "(?s).*Error: error issuing tx with ID(?s).+: failed to decode client response: couldn't issue tx: failed to read consumed(?s).+" + toMatch = "(?s).*Error: error issuing tx with ID(?s).+: failed to decode client response: couldn't issue tx: (?s).+failed to read consumed(?s).+" matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) diff --git a/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoA/suite.go b/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoA/suite.go new file mode 100644 index 000000000..3be59448a --- /dev/null +++ b/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoA/suite.go @@ -0,0 +1,147 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package subnet + +import ( + "fmt" + "regexp" + + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +var blockchainID = "" + +const ( + subnetName = "e2eSubnetTest" + keyName = "ewoq" + ewoqEVMAddress = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ewoqPChainAddress = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + testLocalNodeName = "e2eSubnetTest-local-node" +) + +var _ = ginkgo.Describe("[Etna AddRemove Validator SOV PoA]", func() { + ginkgo.It("Create Etna Subnet Config", func() { + commands.CreateEtnaSubnetEvmConfig( + subnetName, + ewoqEVMAddress, + commands.PoA, + ) + }) + ginkgo.It("Can create a local node connected to Etna Devnet", func() { + output, err := commands.CreateLocalEtnaDevnetNode( + testLocalNodeName, + 7, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Deploy Etna Subnet", func() { + output, err := commands.DeployEtnaSubnetToCluster( + subnetName, + testLocalNodeName, + []string{ + "http://127.0.0.1:9650", + "http://127.0.0.1:9652", + "http://127.0.0.1:9654", + "http://127.0.0.1:9656", + "http://127.0.0.1:9658", + }, + ewoqPChainAddress, + true, // convertOnly + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can make cluster track a subnet", func() { + output, err := commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + // parse blockchainID from output + re := regexp.MustCompile(`Waiting for rpc http://.*?/bc/([A-Za-z0-9]+)/rpc`) + // Find the first match + match := re.FindStringSubmatch(output) + gomega.Expect(match).ToNot(gomega.BeEmpty()) + if len(match) > 1 { + // The first submatch will contain the chain ID + blockchainID = match[1] + } + gomega.Expect(blockchainID).Should(gomega.Not(gomega.BeEmpty())) + ginkgo.GinkgoWriter.Printf("Blockchain ID: %s\n", blockchainID) + }) + + ginkgo.It("Can initialize a PoA Manager contract", func() { + output, err := commands.InitValidatorManager(subnetName, + testLocalNodeName, + "http://127.0.0.1:9650", + blockchainID, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can add validator", func() { + output, err := commands.AddEtnaSubnetValidatorToCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9660", + ewoqPChainAddress, + 1, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can add second validator", func() { + output, err := commands.AddEtnaSubnetValidatorToCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9662", + ewoqPChainAddress, + 1, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can get status of the cluster", func() { + output, err := commands.GetLocalClusterStatus(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + // make sure we can find string with "http://127.0.0.1:9660" and "L1:Validating" string in the output + gomega.Expect(output).To(gomega.MatchRegexp(`http://127\.0\.0\.1:9652.*Validating`), "expect to have L1 validating") + // make sure we can do the same for "http://127.0.0.1:9662" + gomega.Expect(output).To(gomega.MatchRegexp(`http://127\.0\.0\.1:9654.*Validating`), "expect to have L1 validating") + }) + + ginkgo.It("Can remove bootstrap validator", func() { + output, err := commands.RemoveEtnaSubnetValidatorFromCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9654", + keyName, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can remove non-bootstrap validator", func() { + output, err := commands.RemoveEtnaSubnetValidatorFromCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9660", + keyName, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + ginkgo.It("Can destroy local node", func() { + output, err := commands.DestroyLocalNode(testLocalNodeName) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) +}) diff --git a/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoS/suite.go b/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoS/suite.go new file mode 100644 index 000000000..c4a913ad2 --- /dev/null +++ b/tests/e2e/testcases/subnet/sov/addRemoveValidatorPoS/suite.go @@ -0,0 +1,155 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package subnet + +import ( + "fmt" + "regexp" + "time" + + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +var blockchainID = "" + +const ( + CLIBinary = "./bin/avalanche" + subnetName = "e2eSubnetTest" + keyName = "ewoq" + ewoqEVMAddress = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ewoqPChainAddress = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + testLocalNodeName = "e2eSubnetTest-local-node" +) + +var _ = ginkgo.Describe("[Etna AddRemove Validator SOV PoS]", func() { + ginkgo.It("Create Etna Subnet Config", func() { + commands.CreateEtnaSubnetEvmConfig( + subnetName, + ewoqEVMAddress, + commands.PoS, + ) + }) + ginkgo.It("Can create a local node connected to Etna Devnet", func() { + output, err := commands.CreateLocalEtnaDevnetNode( + testLocalNodeName, + 7, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Deploy Etna Subnet", func() { + output, err := commands.DeployEtnaSubnetToCluster( + subnetName, + testLocalNodeName, + []string{ + "http://127.0.0.1:9650", + "http://127.0.0.1:9652", + "http://127.0.0.1:9654", + "http://127.0.0.1:9656", + "http://127.0.0.1:9658", + }, + ewoqPChainAddress, + true, // convertOnly + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can make cluster track a subnet", func() { + output, err := commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + // parse blockchainID from output + re := regexp.MustCompile(`Waiting for rpc http://.*?/bc/([A-Za-z0-9]+)/rpc`) + // Find the first match + match := re.FindStringSubmatch(output) + gomega.Expect(match).ToNot(gomega.BeEmpty()) + if len(match) > 1 { + // The first submatch will contain the chain ID + blockchainID = match[1] + } + gomega.Expect(blockchainID).Should(gomega.Not(gomega.BeEmpty())) + ginkgo.GinkgoWriter.Printf("Blockchain ID: %s\n", blockchainID) + }) + + ginkgo.It("Can initialize a PoS Manager contract", func() { + output, err := commands.InitValidatorManager(subnetName, + testLocalNodeName, + "http://127.0.0.1:9650", + blockchainID, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can add validator", func() { + output, err := commands.AddEtnaSubnetValidatorToCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9660", + ewoqPChainAddress, + 1, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can add second validator", func() { + output, err := commands.AddEtnaSubnetValidatorToCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9662", + ewoqPChainAddress, + 1, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can get status of thecluster", func() { + output, err := commands.GetLocalClusterStatus(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + // make sure we can find string with "http://127.0.0.1:9660" and "L1:Validating" string in the output + gomega.Expect(output).To(gomega.MatchRegexp(`http://127\.0\.0\.1:9652.*Validating`), "expect to have L1 validating") + // make sure we can do the same for "http://127.0.0.1:9662" + gomega.Expect(output).To(gomega.MatchRegexp(`http://127\.0\.0\.1:9654.*Validating`), "expect to have L1 validating") + }) + + ginkgo.It("Can wait for min staking period to pass", func() { + time.Sleep(120 * time.Second) + }) + + /* + ginkgo.It("Can remove non-bootstrap validator", func() { + output, err := commands.RemoveEtnaSubnetValidatorFromCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9662", + keyName, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + */ + ginkgo.It("Can remove bootstrap validator", func() { + output, err := commands.RemoveEtnaSubnetValidatorFromCluster( + testLocalNodeName, + subnetName, + "http://127.0.0.1:9654", + keyName, + ) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) + + ginkgo.It("Can destroy local node", func() { + output, err := commands.DestroyLocalNode(testLocalNodeName) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println(output) + }) +}) diff --git a/tests/e2e/testcases/subnet/sov/etna/suite.go b/tests/e2e/testcases/subnet/sov/etna/suite.go new file mode 100644 index 000000000..ab47d3032 --- /dev/null +++ b/tests/e2e/testcases/subnet/sov/etna/suite.go @@ -0,0 +1,285 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package subnet + +import ( + "fmt" + "os" + "os/exec" + + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + CLIBinary = "./bin/avalanche" + subnetName = "e2eSubnetTest" + keyName = "ewoq" + avalancheGoPath = "--avalanchego-path" + ewoqEVMAddress = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ewoqPChainAddress = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + testLocalNodeName = "e2eSubnetTest-local-node" +) + +func createEtnaSubnetEvmConfig(poa, pos bool) { + // Check config does not already exist + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeFalse()) + + cmdArgs := []string{ + "blockchain", + "create", + subnetName, + "--evm", + "--validator-manager-owner", + ewoqEVMAddress, + "--proxy-contract-owner", + ewoqEVMAddress, + "--production-defaults", + "--evm-chain-id=99999", + "--evm-token=TOK", + "--" + constants.SkipUpdateFlag, + } + if poa { + cmdArgs = append(cmdArgs, "--proof-of-authority") + } else if pos { + cmdArgs = append(cmdArgs, "--proof-of-stake") + } + + cmd := exec.Command(CLIBinary, cmdArgs...) + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + + // Config should now exist + exists, err = utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) +} + +func destroyLocalNode() { + _, err := os.Stat(testLocalNodeName) + if os.IsNotExist(err) { + return + } + cmd := exec.Command( + CLIBinary, + "node", + "local", + "destroy", + testLocalNodeName, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) +} + +func deployEtnaSubnetEtnaFlag() { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // Deploy subnet on etna devnet with local machine as bootstrap validator + cmd := exec.Command( + CLIBinary, + "blockchain", + "deploy", + subnetName, + "--etna-devnet", + "--use-local-machine", + "--num-local-nodes=1", + "--ewoq", + "--change-owner-address", + ewoqPChainAddress, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) +} + +func deployEtnaSubnetEtnaFlagConvertOnly() { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // Deploy subnet on etna devnet with local machine as bootstrap validator + cmd := exec.Command( + CLIBinary, + "blockchain", + "deploy", + subnetName, + "--etna-devnet", + "--use-local-machine", + "--num-local-nodes=1", + "--convert-only", + "--ewoq", + "--change-owner-address", + ewoqPChainAddress, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) +} + +func deployEtnaSubnetClusterFlagConvertOnly(clusterName string) { + // Check config exists + exists, err := utils.SubnetConfigExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeTrue()) + + // Deploy subnet on etna devnet with local machine as bootstrap validator + cmd := exec.Command( + CLIBinary, + "blockchain", + "deploy", + subnetName, + fmt.Sprintf("--cluster=%s", clusterName), + "--convert-only", + "--ewoq", + "--change-owner-address", + ewoqPChainAddress, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) +} + +func initValidatorManagerClusterFlag( + subnetName string, + clusterName string, +) error { + cmd := exec.Command( + CLIBinary, + "contract", + "initValidatorManager", + subnetName, + "--cluster", + clusterName, + "--genesis-key", + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return err +} + +func initValidatorManagerEtnaFlag( + subnetName string, +) (string, error) { + cmd := exec.Command( + CLIBinary, + "contract", + "initValidatorManager", + subnetName, + "--etna-devnet", + "--genesis-key", + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + return string(output), err +} + +var _ = ginkgo.Describe("[Etna Subnet SOV]", func() { + ginkgo.BeforeEach(func() { + // key + _ = utils.DeleteKey(keyName) + output, err := commands.CreateKeyFromPath(keyName, utils.EwoqKeyPath) + if err != nil { + fmt.Println(output) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + // subnet config + _ = utils.DeleteConfigs(subnetName) + destroyLocalNode() + }) + + ginkgo.AfterEach(func() { + destroyLocalNode() + commands.DeleteSubnetConfig(subnetName) + err := utils.DeleteKey(keyName) + gomega.Expect(err).Should(gomega.BeNil()) + commands.CleanNetwork() + }) + ginkgo.It("Create Etna POA Subnet Config & Deploy the Subnet To Public Etna On Local Machine", func() { + createEtnaSubnetEvmConfig(true, false) + deployEtnaSubnetEtnaFlag() + }) + + ginkgo.It("Create Etna POS Subnet Config & Deploy the Subnet To Public Etna On Local Machine", func() { + createEtnaSubnetEvmConfig(false, true) + deployEtnaSubnetEtnaFlag() + }) + + ginkgo.It("Start Local Node on Etna & Deploy the Subnet To Public Etna using cluster flag", func() { + _, err := commands.CreateLocalEtnaDevnetNode(testLocalNodeName, 1) + gomega.Expect(err).Should(gomega.BeNil()) + createEtnaSubnetEvmConfig(true, false) + deployEtnaSubnetClusterFlagConvertOnly(testLocalNodeName) + _, err = commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + err = initValidatorManagerClusterFlag(subnetName, testLocalNodeName) + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.It("Mix and match network and cluster flags test 1", func() { + _, err := commands.CreateLocalEtnaDevnetNode(testLocalNodeName, 1) + gomega.Expect(err).Should(gomega.BeNil()) + createEtnaSubnetEvmConfig(true, false) + deployEtnaSubnetClusterFlagConvertOnly(testLocalNodeName) + _, err = commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + _, err = initValidatorManagerEtnaFlag(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + }) + ginkgo.It("Mix and match network and cluster flags test 2", func() { + createEtnaSubnetEvmConfig(true, false) + deployEtnaSubnetEtnaFlagConvertOnly() + _, err := commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + err = initValidatorManagerClusterFlag(subnetName, testLocalNodeName) + gomega.Expect(err).Should(gomega.BeNil()) + }) +}) diff --git a/tests/e2e/testcases/subnet/local/suite.go b/tests/e2e/testcases/subnet/sov/local/suite.go similarity index 78% rename from tests/e2e/testcases/subnet/local/suite.go rename to tests/e2e/testcases/subnet/sov/local/suite.go index 6c4e445d7..be24be532 100644 --- a/tests/e2e/testcases/subnet/local/suite.go +++ b/tests/e2e/testcases/subnet/sov/local/suite.go @@ -25,11 +25,6 @@ const ( subnetName = "e2eSubnetTest" secondSubnetName = "e2eSecondSubnetTest" confPath = "tests/e2e/assets/test_avalanche-cli.json" - stakeAmount = "2000" - stakeDuration = "337h" - delegateAmount = "25" - delegateDuration = "336h" - localNetwork = "Local Network" ) var ( @@ -37,7 +32,7 @@ var ( err error ) -var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { +var _ = ginkgo.Describe("[Local Subnet SOV]", ginkgo.Ordered, func() { _ = ginkgo.BeforeAll(func() { mapper := utils.NewVersionMapper() mapping, err = utils.GetVersionMapping(mapper) @@ -62,11 +57,11 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { utils.DeleteCustomBinary(secondSubnetName) }) - ginkgo.It("can deploy a custom vm subnet to local", func() { + ginkgo.It("can deploy a custom vm subnet to local SOV", func() { customVMPath, err := utils.DownloadCustomVMBin(mapping[utils.SoloSubnetEVMKey1]) gomega.Expect(err).Should(gomega.BeNil()) - commands.CreateCustomVMConfig(subnetName, utils.SubnetEvmGenesisPath, customVMPath) - deployOutput := commands.DeploySubnetLocallyWithVersion(subnetName, mapping[utils.SoloAvagoKey]) + commands.CreateCustomVMConfigSOV(subnetName, utils.SubnetEvmGenesisPath, customVMPath) + deployOutput := commands.DeploySubnetLocallyWithVersionSOV(subnetName, mapping[utils.SoloAvagoKey]) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -84,9 +79,9 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can deploy a SubnetEvm subnet to local", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocally(subnetName) + ginkgo.It("can deploy a SubnetEvm subnet to local SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -104,9 +99,9 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can load viper config and setup node properties for local deploy", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocallyWithViperConf(subnetName, confPath) + ginkgo.It("can load viper config and setup node properties for local deploy SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallyWithViperConfSOV(subnetName, confPath) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -114,15 +109,15 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(rpcs).Should(gomega.HaveLen(1)) rpc := rpcs[0] - gomega.Expect(rpc).Should(gomega.HavePrefix("http://0.0.0.0:")) + gomega.Expect(rpc).Should(gomega.HavePrefix("http://127.0.0.1:")) commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can't deploy the same subnet twice to local", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can't deploy the same subnet twice to local SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) fmt.Println(deployOutput) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { @@ -131,7 +126,7 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(rpcs).Should(gomega.HaveLen(1)) - out, err := commands.DeploySubnetLocallyWithArgsAndOutput(subnetName, "", "") + out, err := commands.DeploySubnetLocallyWithArgsAndOutputSOV(subnetName, "", "") gomega.Expect(err).Should(gomega.HaveOccurred()) deployOutput = string(out) rpcs, err = utils.ParseRPCsFromOutput(deployOutput) @@ -143,11 +138,11 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { gomega.Expect(deployOutput).Should(gomega.ContainSubstring("has already been deployed")) }) - ginkgo.It("can deploy multiple subnets to local", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) - commands.CreateSubnetEvmConfig(secondSubnetName, utils.SubnetEvmGenesis2Path) + ginkgo.It("can deploy multiple subnets to local SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + commands.CreateSubnetEvmConfigSOV(secondSubnetName, utils.SubnetEvmGenesis2Path) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs1, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -155,7 +150,7 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(rpcs1).Should(gomega.HaveLen(1)) - deployOutput = commands.DeploySubnetLocally(secondSubnetName) + deployOutput = commands.DeploySubnetLocallySOV(secondSubnetName) rpcs2, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -179,8 +174,8 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(secondSubnetName) }) - ginkgo.It("can deploy custom chain config", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmAllowFeeRecpPath) + ginkgo.It("can deploy custom chain config SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmAllowFeeRecpPath) addr := "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" @@ -194,7 +189,7 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.ConfigureChainConfig(subnetName, file.Name()) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -224,8 +219,8 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can deploy with custom per chain config node", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can deploy with custom per chain config node SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) // create per node chain config nodesRPCTxFeeCap := map[string]string{ @@ -255,7 +250,7 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.ConfigurePerNodeChainConfig(subnetName, file.Name()) // deploy - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -283,7 +278,7 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can list a subnet's validators", func() { + ginkgo.It("can list a subnet's validators SOV", func() { nodeIDs := []string{ "NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5", "NodeID-GWPcbFJZFfZreETSoWjPimr846mXEKCtu", @@ -292,8 +287,8 @@ var _ = ginkgo.Describe("[Local Subnet]", ginkgo.Ordered, func() { "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg", } - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocally(subnetName) + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) _, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -328,8 +323,8 @@ var _ = ginkgo.Describe("[Subnet Compatibility]", func() { ginkgo.It("can deploy a subnet-evm with old version", func() { subnetEVMVersion := "v0.6.9" - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion) - deployOutput := commands.DeploySubnetLocally(subnetName) + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -352,10 +347,10 @@ var _ = ginkgo.Describe("[Subnet Compatibility]", func() { subnetEVMVersion1 := "v0.6.9" subnetEVMVersion2 := "v0.6.8" - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) - commands.CreateSubnetEvmConfigWithVersion(secondSubnetName, utils.SubnetEvmGenesis2Path, subnetEVMVersion2) + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) + commands.CreateSubnetEvmConfigWithVersionSOV(secondSubnetName, utils.SubnetEvmGenesis2Path, subnetEVMVersion2) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -363,7 +358,7 @@ var _ = ginkgo.Describe("[Subnet Compatibility]", func() { gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(rpcs).Should(gomega.HaveLen(1)) - commands.DeploySubnetLocallyExpectError(secondSubnetName) + commands.DeploySubnetLocallyExpectErrorSOV(secondSubnetName) commands.DeleteSubnetConfig(subnetName) commands.DeleteSubnetConfig(secondSubnetName) diff --git a/tests/e2e/testcases/subnet/sov/public/suite.go b/tests/e2e/testcases/subnet/sov/public/suite.go new file mode 100644 index 000000000..fbc2b91de --- /dev/null +++ b/tests/e2e/testcases/subnet/sov/public/suite.go @@ -0,0 +1,416 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package subnet + +import ( + "fmt" + "os" + "regexp" + "strings" + "time" + + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/subnet" + cliutils "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + subnetName = "e2eSubnetTest" + controlKeys = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + keyName = "ewoq" + ledger1Seed = "ledger1" + ledger2Seed = "ledger2" + ledger3Seed = "ledger3" + txFnamePrefix = "avalanche-cli-tx-" + mainnetChainID = 123456 +) + +func deploySubnetToFujiSOV() (string, map[string]utils.NodeInfo) { + // deploy + s := commands.SimulateFujiDeploySOV(subnetName, keyName, controlKeys) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) + gomega.Expect(err).Should(gomega.BeNil()) + // add validators to subnet + nodeInfos, err := utils.GetNodesInfo() + gomega.Expect(err).Should(gomega.BeNil()) + for _, nodeInfo := range nodeInfos { + start := time.Now().Add(time.Second * 30).UTC().Format("2006-01-02 15:04:05") + _ = commands.SimulateFujiAddValidator(subnetName, keyName, nodeInfo.ID, start, "24h", "20") + } + // join to copy vm binary and update config file + for _, nodeInfo := range nodeInfos { + _ = commands.SimulateFujiJoin(subnetName, nodeInfo.ConfigFile, nodeInfo.PluginDir, nodeInfo.ID) + } + // get and check whitelisted subnets from config file + var whitelistedSubnets string + for _, nodeInfo := range nodeInfos { + whitelistedSubnets, err = utils.GetWhitelistedSubnetsFromConfigFile(nodeInfo.ConfigFile) + gomega.Expect(err).Should(gomega.BeNil()) + whitelistedSubnetsSlice := strings.Split(whitelistedSubnets, ",") + gomega.Expect(whitelistedSubnetsSlice).Should(gomega.ContainElement(subnetID)) + } + // update nodes whitelisted subnets + err = utils.RestartNodesWithWhitelistedSubnets(whitelistedSubnets) + gomega.Expect(err).Should(gomega.BeNil()) + // wait for subnet walidators to be up + err = utils.WaitSubnetValidators(subnetID, nodeInfos) + gomega.Expect(err).Should(gomega.BeNil()) + return subnetID, nodeInfos +} + +var _ = ginkgo.Describe("[Public Subnet SOV]", func() { + ginkgo.BeforeEach(func() { + // key + _ = utils.DeleteKey(keyName) + output, err := commands.CreateKeyFromPath(keyName, utils.EwoqKeyPath) + if err != nil { + fmt.Println(output) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + // subnet config + _ = utils.DeleteConfigs(subnetName) + _, avagoVersion := commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + + // local network + commands.StartNetworkWithVersion(avagoVersion) + }) + + ginkgo.AfterEach(func() { + commands.DeleteSubnetConfig(subnetName) + err := utils.DeleteKey(keyName) + gomega.Expect(err).Should(gomega.BeNil()) + commands.CleanNetwork() + }) + + ginkgo.It("deploy subnet to fuji SOV", func() { + deploySubnetToFujiSOV() + }) + + ginkgo.It("deploy subnet to mainnet SOV", func() { + var interactionEndCh, ledgerSimEndCh chan struct{} + if os.Getenv("LEDGER_SIM") != "" { + interactionEndCh, ledgerSimEndCh = utils.StartLedgerSim(7, ledger1Seed, true) + } + // fund ledger address + genesisParams := genesis.MainnetParams + err := utils.FundLedgerAddress(genesisParams.TxFeeConfig.StaticFeeConfig.CreateSubnetTxFee + genesisParams.TxFeeConfig.StaticFeeConfig.CreateBlockchainTxFee + genesisParams.TxFeeConfig.StaticFeeConfig.TxFee) + gomega.Expect(err).Should(gomega.BeNil()) + fmt.Println() + fmt.Println(logging.LightRed.Wrap("DEPLOYING SUBNET. VERIFY LEDGER ADDRESS HAS CUSTOM HRP BEFORE SIGNING")) + s := commands.SimulateMainnetDeploySOV(subnetName, 0, false) + // deploy + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) + gomega.Expect(err).Should(gomega.BeNil()) + // add validators to subnet + nodeInfos, err := utils.GetNodesInfo() + gomega.Expect(err).Should(gomega.BeNil()) + nodeIdx := 1 + for _, nodeInfo := range nodeInfos { + fmt.Println(logging.LightRed.Wrap( + fmt.Sprintf("ADDING VALIDATOR %d of %d. VERIFY LEDGER ADDRESS HAS CUSTOM HRP BEFORE SIGNING", nodeIdx, len(nodeInfos)))) + start := time.Now().Add(time.Second * 30).UTC().Format("2006-01-02 15:04:05") + _ = commands.SimulateMainnetAddValidator(subnetName, nodeInfo.ID, start, "24h", "20") + nodeIdx++ + } + if os.Getenv("LEDGER_SIM") != "" { + close(interactionEndCh) + <-ledgerSimEndCh + } + fmt.Println(logging.LightBlue.Wrap("EXECUTING NON INTERACTIVE PART OF THE TEST: JOIN/WHITELIST/WAIT/HARDHAT")) + // join to copy vm binary and update config file + for _, nodeInfo := range nodeInfos { + _ = commands.SimulateMainnetJoin(subnetName, nodeInfo.ConfigFile, nodeInfo.PluginDir, nodeInfo.ID) + } + // get and check whitelisted subnets from config file + var whitelistedSubnets string + for _, nodeInfo := range nodeInfos { + whitelistedSubnets, err = utils.GetWhitelistedSubnetsFromConfigFile(nodeInfo.ConfigFile) + gomega.Expect(err).Should(gomega.BeNil()) + whitelistedSubnetsSlice := strings.Split(whitelistedSubnets, ",") + gomega.Expect(whitelistedSubnetsSlice).Should(gomega.ContainElement(subnetID)) + } + // update nodes whitelisted subnets + err = utils.RestartNodesWithWhitelistedSubnets(whitelistedSubnets) + gomega.Expect(err).Should(gomega.BeNil()) + // wait for subnet walidators to be up + err = utils.WaitSubnetValidators(subnetID, nodeInfos) + gomega.Expect(err).Should(gomega.BeNil()) + + // this is a simulation, so app is probably saving the info in the + // `local network` section of the sidecar instead of the `fuji` section... + // ...need to manipulate the `fuji` section of the sidecar to contain the subnetID info + // so that the `stats` command for `fuji` can find it + output := commands.SimulateGetSubnetStatsFuji(subnetName, subnetID) + gomega.Expect(output).Should(gomega.Not(gomega.BeNil())) + gomega.Expect(output).Should(gomega.ContainSubstring("Current validators")) + gomega.Expect(output).Should(gomega.ContainSubstring("NodeID-")) + }) + + ginkgo.It("deploy subnet with new chain id SOV", func() { + subnetMainnetChainID, err := utils.GetSubnetEVMMainneChainID(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(subnetMainnetChainID).Should(gomega.Equal(uint(0))) + _ = commands.SimulateMainnetDeploySOV(subnetName, mainnetChainID, true) + subnetMainnetChainID, err = utils.GetSubnetEVMMainneChainID(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(subnetMainnetChainID).Should(gomega.Equal(uint(mainnetChainID))) + }) + + ginkgo.It("remove validator fuji SOV", func() { + subnetIDStr, nodeInfos := deploySubnetToFujiSOV() + + // pick a validator to remove + var validatorToRemove string + for _, nodeInfo := range nodeInfos { + validatorToRemove = nodeInfo.ID + break + } + + // confirm current validator set + subnetID, err := ids.FromString(subnetIDStr) + gomega.Expect(err).Should(gomega.BeNil()) + validators, err := subnet.GetSubnetValidators(subnetID) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(len(validators)).Should(gomega.Equal(5)) + + // Check that the validatorToRemove is in the subnet validator set + var found bool + for _, validator := range validators { + if validator.NodeID.String() == validatorToRemove { + found = true + break + } + } + gomega.Expect(found).Should(gomega.BeTrue()) + + // remove validator + _ = commands.SimulateFujiRemoveValidator(subnetName, keyName, validatorToRemove) + + // confirm current validator set + validators, err = subnet.GetSubnetValidators(subnetID) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(len(validators)).Should(gomega.Equal(4)) + + // Check that the validatorToRemove is NOT in the subnet validator set + found = false + for _, validator := range validators { + if validator.NodeID.String() == validatorToRemove { + found = true + break + } + } + gomega.Expect(found).Should(gomega.BeFalse()) + }) + + ginkgo.It("mainnet multisig deploy SOV", func() { + // this is not expected to be executed with real ledgers + // as that will complicate too much the test flow + gomega.Expect(os.Getenv("LEDGER_SIM")).Should(gomega.Equal("true"), "multisig test not designed for real ledgers: please set env var LEDGER_SIM to true") + + txPath, err := utils.GetTmpFilePath(txFnamePrefix) + gomega.Expect(err).Should(gomega.BeNil()) + + // obtain ledger2 addr + interactionEndCh, ledgerSimEndCh := utils.StartLedgerSim(0, ledger2Seed, false) + ledger2Addr, err := utils.GetLedgerAddress(models.NewLocalNetwork(), 0) + gomega.Expect(err).Should(gomega.BeNil()) + close(interactionEndCh) + <-ledgerSimEndCh + + // obtain ledger3 addr + interactionEndCh, ledgerSimEndCh = utils.StartLedgerSim(0, ledger3Seed, false) + ledger3Addr, err := utils.GetLedgerAddress(models.NewLocalNetwork(), 0) + gomega.Expect(err).Should(gomega.BeNil()) + close(interactionEndCh) + <-ledgerSimEndCh + + // ledger4 addr + // will not be used to sign, only as a extra control key, so no sim is needed to generate it + ledger4Addr := "P-custom18g2tekxzt60j3sn8ymjx6qvk96xunhctkyzckt" + + // start the deploy process with ledger1 + interactionEndCh, ledgerSimEndCh = utils.StartLedgerSim(2, ledger1Seed, true) + + // obtain ledger1 addr + ledger1Addr, err := utils.GetLedgerAddress(models.NewLocalNetwork(), 0) + gomega.Expect(err).Should(gomega.BeNil()) + + // multisig deploy from unfunded ledger1 should not create any subnet/blockchain + gomega.Expect(err).Should(gomega.BeNil()) + s := commands.SimulateMultisigMainnetDeploySOV( + subnetName, + []string{ledger2Addr, ledger3Addr, ledger4Addr}, + []string{ledger2Addr, ledger3Addr}, + txPath, + true, + ) + toMatch := "(?s).+Not enough funds in the first (?s).+ indices of Ledger(?s).+Error: not enough funds on ledger(?s).+" + matched, err := regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // let's fund the ledger + genesisParams := genesis.MainnetParams + err = utils.FundLedgerAddress(genesisParams.TxFeeConfig.StaticFeeConfig.CreateSubnetTxFee + genesisParams.TxFeeConfig.StaticFeeConfig.CreateBlockchainTxFee + genesisParams.TxFeeConfig.StaticFeeConfig.TxFee) + gomega.Expect(err).Should(gomega.BeNil()) + + // multisig deploy from funded ledger1 should create the subnet but not deploy the blockchain, + // instead signing only its tx fee as it is not a subnet auth key, + // and creating the tx file to wait for subnet auths from ledger2 and ledger3 + s = commands.SimulateMultisigMainnetDeploySOV( + subnetName, + []string{ledger2Addr, ledger3Addr, ledger4Addr}, + []string{ledger2Addr, ledger3Addr}, + txPath, + false, + ) + toMatch = "(?s).+Ledger addresses:(?s).+ " + ledger1Addr + "(?s).+Subnet has been created with ID(?s).+" + //nolint:goconst + "0 of 2 required Blockchain Creation signatures have been signed\\. Saving tx to disk to enable remaining signing\\.(?s).+" + + "Addresses remaining to sign the tx\\s+" + ledger2Addr + "(?s).+" + ledger3Addr + "(?s).+" //nolint:goconst + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // try to commit before signature is complete (no funded wallet needed for commit) + s = commands.TransactionCommit( + subnetName, + txPath, + true, + ) + toMatch = "(?s).*0 of 2 required signatures have been signed\\.(?s).+" + + "Addresses remaining to sign the tx\\s+" + ledger2Addr + "(?s).+" + ledger3Addr + "(?s).+" + + "(?s).+Error: tx is not fully signed(?s).+" //nolint:goconst + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // try to sign using unauthorized ledger1 + s = commands.TransactionSignWithLedger( + subnetName, + txPath, + true, + ) + toMatch = "(?s).+Ledger addresses:(?s).+ " + ledger1Addr + "(?s).+There are no required subnet auth keys present in the wallet(?s).+" + + "Expected one of:\\s+" + ledger2Addr + "(?s).+" + ledger3Addr + "(?s).+Error: no remaining signer address present in wallet.*" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // wait for end of ledger1 simulation + close(interactionEndCh) + <-ledgerSimEndCh + + // try to commit before signature is complete + s = commands.TransactionCommit( + subnetName, + txPath, + true, + ) + toMatch = "(?s).*0 of 2 required signatures have been signed\\.(?s).+" + + "Addresses remaining to sign the tx\\s+" + ledger2Addr + "(?s).+" + ledger3Addr + "(?s).+" + + "(?s).+Error: tx is not fully signed(?s).+" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // sign using ledger2 + interactionEndCh, ledgerSimEndCh = utils.StartLedgerSim(1, ledger2Seed, true) + s = commands.TransactionSignWithLedger( + subnetName, + txPath, + false, + ) + toMatch = "(?s).+Ledger addresses:(?s).+ " + ledger2Addr + "(?s).+1 of 2 required Tx signatures have been signed\\.(?s).+" + + "Addresses remaining to sign the tx\\s+" + ledger3Addr + ".*" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // try to sign using ledger2 which already signed + s = commands.TransactionSignWithLedger( + subnetName, + txPath, + true, + ) + toMatch = "(?s).+Ledger addresses:(?s).+ " + ledger2Addr + "(?s).+There are no required subnet auth keys present in the wallet(?s).+" + + "Expected one of:\\s+" + ledger3Addr + "(?s).+Error: no remaining signer address present in wallet.*" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // wait for end of ledger2 simulation + close(interactionEndCh) + <-ledgerSimEndCh + + // try to commit before signature is complete + s = commands.TransactionCommit( + subnetName, + txPath, + true, + ) + toMatch = "(?s).*1 of 2 required signatures have been signed\\.(?s).+" + + "Addresses remaining to sign the tx\\s+" + ledger3Addr + + "(?s).+Error: tx is not fully signed(?s).+" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // sign with ledger3 + interactionEndCh, ledgerSimEndCh = utils.StartLedgerSim(1, ledger3Seed, true) + s = commands.TransactionSignWithLedger( + subnetName, + txPath, + false, + ) + toMatch = "(?s).+Ledger addresses:(?s).+ " + ledger3Addr + "(?s).+Tx is fully signed, and ready to be committed(?s).+" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // try to sign using ledger3 which already signedtx is already fully signed" + s = commands.TransactionSignWithLedger( + subnetName, + txPath, + true, + ) + toMatch = "(?s).*Tx is fully signed, and ready to be committed(?s).+Error: tx is already fully signed" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // wait for end of ledger3 simulation + close(interactionEndCh) + <-ledgerSimEndCh + + // commit after complete signature + s = commands.TransactionCommit( + subnetName, + txPath, + false, + ) + toMatch = "(?s).+DEPLOYMENT RESULTS(?s).+Blockchain ID(?s).+" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + + // try to commit again + s = commands.TransactionCommit( + subnetName, + txPath, + true, + ) + toMatch = "(?s).*Error: error issuing tx with ID(?s).+: failed to decode client response: couldn't issue tx: failed to read consumed(?s).+" + matched, err = regexp.MatchString(toMatch, cliutils.RemoveLineCleanChars(s)) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(matched).Should(gomega.Equal(true), "no match between command output %q and pattern %q", s, toMatch) + }) +}) diff --git a/tests/e2e/testcases/subnet/suite.go b/tests/e2e/testcases/subnet/suite.go index 4d66dfd29..3781f3eb1 100644 --- a/tests/e2e/testcases/subnet/suite.go +++ b/tests/e2e/testcases/subnet/suite.go @@ -24,17 +24,34 @@ var _ = ginkgo.Describe("[Subnet]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) }) - ginkgo.It("can create and delete a subnet evm config", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can create and delete a subnet evm config non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can create and delete a custom vm subnet config", func() { + ginkgo.It("can create and delete a subnet evm config SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can create and delete a custom vm subnet config non SOV", func() { + // let's use a SubnetEVM version which would be compatible with an existing Avago + customVMPath, err := utils.DownloadCustomVMBin(mapping[utils.SoloSubnetEVMKey1]) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.CreateCustomVMConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath, customVMPath) + commands.DeleteSubnetConfig(subnetName) + exists, err := utils.SubnetCustomVMExists(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(exists).Should(gomega.BeFalse()) + }) + + ginkgo.It("can create and delete a custom vm subnet config SOV", func() { // let's use a SubnetEVM version which would be compatible with an existing Avago customVMPath, err := utils.DownloadCustomVMBin(mapping[utils.SoloSubnetEVMKey1]) gomega.Expect(err).Should(gomega.BeNil()) - commands.CreateCustomVMConfig(subnetName, utils.SubnetEvmGenesisPath, customVMPath) + commands.CreateCustomVMConfigSOV(subnetName, utils.SubnetEvmGenesisPath, customVMPath) commands.DeleteSubnetConfig(subnetName) exists, err := utils.SubnetCustomVMExists(subnetName) gomega.Expect(err).Should(gomega.BeNil()) diff --git a/tests/e2e/testcases/upgrade/non-sov/suite.go b/tests/e2e/testcases/upgrade/non-sov/suite.go new file mode 100644 index 000000000..0c3a8e4d2 --- /dev/null +++ b/tests/e2e/testcases/upgrade/non-sov/suite.go @@ -0,0 +1,459 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package apm + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + "unicode" + + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd/upgradecmd" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/binutils" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + anr_utils "github.com/ava-labs/avalanche-network-runner/utils" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/subnet-evm/params" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + subnetName = "e2eSubnetTest" + secondSubnetName = "e2eSecondSubnetTest" + + subnetEVMVersion1 = "v0.6.9" + subnetEVMVersion2 = "v0.6.10" + + avagoRPC1Version = "v1.11.11" + avagoRPC2Version = "v1.11.11" + + controlKeys = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + keyName = "ewoq" + + upgradeBytesPath = "tests/e2e/assets/test_upgrade.json" + + upgradeBytesPath2 = "tests/e2e/assets/test_upgrade_2.json" +) + +var ( + binaryToVersion map[string]string + err error +) + +// need to have this outside the normal suite because of the BeforeEach +var _ = ginkgo.Describe("[Upgrade expect network failure non SOV]", ginkgo.Ordered, func() { + ginkgo.AfterEach(func() { + commands.CleanNetworkHard() + err := utils.DeleteConfigs(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.It("fails on stopped network non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + + // we want to simulate a situation here where the subnet has been deployed + // but the network is stopped + // the code would detect it hasn't been deployed yet so report that error first + // therefore we can just manually edit the file to fake it had been deployed + app := application.New() + app.Setup(utils.GetBaseDir(), logging.NoLog{}, nil, nil, nil) + sc := models.Sidecar{ + Name: subnetName, + Subnet: subnetName, + Networks: make(map[string]models.NetworkData), + } + sc.Networks[models.Local.String()] = models.NetworkData{ + SubnetID: ids.GenerateTestID(), + BlockchainID: ids.GenerateTestID(), + } + err = app.UpdateSidecar(&sc) + gomega.Expect(err).Should(gomega.BeNil()) + + out, err := commands.ApplyUpgradeLocal(subnetName) + gomega.Expect(err).Should(gomega.HaveOccurred()) + gomega.Expect(out).Should(gomega.ContainSubstring(binutils.ErrGRPCTimeout.Error())) + }) +}) + +// upgrade a public network +// the approach is rather simple: import the upgrade file, +// call the apply command which "just" installs the file at an expected path, +// and then check the file is there and has the correct content. +var _ = ginkgo.Describe("[Upgrade public network non SOV]", ginkgo.Ordered, func() { + ginkgo.AfterEach(func() { + commands.CleanNetworkHard() + err := utils.DeleteConfigs(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.It("can create and apply to public node non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + // simulate as if this had already been deployed to fuji + // by just entering fake data into the struct + app := application.New() + app.Setup(utils.GetBaseDir(), logging.NoLog{}, nil, nil, nil) + + sc, err := app.LoadSidecar(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + blockchainID := ids.GenerateTestID() + sc.Networks = make(map[string]models.NetworkData) + sc.Networks[models.Fuji.String()] = models.NetworkData{ + SubnetID: ids.GenerateTestID(), + BlockchainID: blockchainID, + } + err = app.UpdateSidecar(&sc) + gomega.Expect(err).Should(gomega.BeNil()) + + // import the upgrade bytes file so have one + _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + + // we'll set a fake chain config dir to not mess up with a potential real one + // in the system + avalanchegoConfigDir, err := os.MkdirTemp("", "cli-tmp-avago-conf-dir") + gomega.Expect(err).Should(gomega.BeNil()) + defer os.RemoveAll(avalanchegoConfigDir) + + // now we try to apply + _, err = commands.ApplyUpgradeToPublicNode(subnetName, avalanchegoConfigDir) + gomega.Expect(err).Should(gomega.BeNil()) + + // we expect the file to be present at the expected location and being + // the same content as the original one + expectedPath := filepath.Join(avalanchegoConfigDir, blockchainID.String(), constants.UpgradeFileName) + gomega.Expect(expectedPath).Should(gomega.BeARegularFile()) + ori, err := os.ReadFile(upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + cp, err := os.ReadFile(expectedPath) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(ori).Should(gomega.Equal(cp)) + }) +}) + +var _ = ginkgo.Describe("[Upgrade local network non SOV]", ginkgo.Ordered, func() { + _ = ginkgo.BeforeAll(func() { + mapper := utils.NewVersionMapper() + binaryToVersion, err = utils.GetVersionMapping(mapper) + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.BeforeEach(func() { + output, err := commands.CreateKeyFromPath(keyName, utils.EwoqKeyPath) + if err != nil { + fmt.Println(output) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.AfterEach(func() { + commands.CleanNetworkHard() + err := utils.DeleteConfigs(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + err = utils.DeleteConfigs(secondSubnetName) + gomega.Expect(err).Should(gomega.BeNil()) + _ = utils.DeleteKey(keyName) + utils.DeleteCustomBinary(subnetName) + }) + + ginkgo.It("fails on undeployed subnet non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + + _ = commands.StartNetwork() + + out, err := commands.ApplyUpgradeLocal(subnetName) + gomega.Expect(err).Should(gomega.HaveOccurred()) + gomega.Expect(out).Should(gomega.ContainSubstring(upgradecmd.ErrSubnetNotDeployedOutput)) + }) + + ginkgo.It("can create and apply to locally running subnet non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + + _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + + _, err = commands.ApplyUpgradeLocal(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + upgradeBytes, err := os.ReadFile(upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + + var precmpUpgrades params.UpgradeConfig + err = json.Unmarshal(upgradeBytes, &precmpUpgrades) + gomega.Expect(err).Should(gomega.BeNil()) + + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + err = utils.CheckUpgradeIsDeployed(rpcs[0], precmpUpgrades) + gomega.Expect(err).Should(gomega.BeNil()) + + app := application.New() + app.Setup(utils.GetBaseDir(), logging.NoLog{}, nil, nil, nil) + + stripped := stripWhitespaces(string(upgradeBytes)) + lockUpgradeBytes, err := app.ReadLockUpgradeFile(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect([]byte(stripped)).Should(gomega.Equal(lockUpgradeBytes)) + }) + + ginkgo.It("can't upgrade transactionAllowList precompile because admin address doesn't have enough token non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + commands.DeploySubnetLocallyNonSOV(subnetName) + + _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath2) + gomega.Expect(err).Should(gomega.BeNil()) + + _, err = commands.ApplyUpgradeLocal(subnetName) + gomega.Expect(err).Should(gomega.HaveOccurred()) + }) + + ginkgo.It("can upgrade transactionAllowList precompile because admin address has enough tokens non SOV", func() { + commands.CreateSubnetEvmConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath) + + commands.DeploySubnetLocallyNonSOV(subnetName) + + _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) + gomega.Expect(err).Should(gomega.BeNil()) + + _, err = commands.ApplyUpgradeLocal(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + }) + + ginkgo.It("can create and update future non SOV", func() { + subnetEVMVersion1 := binaryToVersion[utils.SoloSubnetEVMKey1] + subnetEVMVersion2 := binaryToVersion[utils.SoloSubnetEVMKey2] + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) + + // check version + output, err := commands.DescribeSubnet(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + containsVersion1 := strings.Contains(output, subnetEVMVersion1) + containsVersion2 := strings.Contains(output, subnetEVMVersion2) + gomega.Expect(containsVersion1).Should(gomega.BeTrue()) + gomega.Expect(containsVersion2).Should(gomega.BeFalse()) + + _, err = commands.UpgradeVMConfig(subnetName, subnetEVMVersion2) + gomega.Expect(err).Should(gomega.BeNil()) + + output, err = commands.DescribeSubnet(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + containsVersion1 = strings.Contains(output, subnetEVMVersion1) + containsVersion2 = strings.Contains(output, subnetEVMVersion2) + gomega.Expect(containsVersion1).Should(gomega.BeFalse()) + gomega.Expect(containsVersion2).Should(gomega.BeTrue()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("upgrade SubnetEVM local deployment non SOV", func() { + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + + // check running version + // remove string suffix starting with /ext + nodeURI := strings.Split(rpcs[0], "/ext")[0] + vmid, err := anr_utils.VMID(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + version, err := utils.GetNodeVMVersion(nodeURI, vmid.String()) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(version).Should(gomega.Equal(subnetEVMVersion1)) + + // stop network + commands.StopNetwork() + + // upgrade + commands.UpgradeVMLocal(subnetName, subnetEVMVersion2) + + // restart network + commands.StartNetwork() + + // check running version + version, err = utils.GetNodeVMVersion(nodeURI, vmid.String()) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(version).Should(gomega.Equal(subnetEVMVersion2)) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("upgrade custom vm local deployment non SOV", func() { + // download vm bins + customVMPath1, err := utils.DownloadCustomVMBin(subnetEVMVersion1) + gomega.Expect(err).Should(gomega.BeNil()) + customVMPath2, err := utils.DownloadCustomVMBin(subnetEVMVersion2) + gomega.Expect(err).Should(gomega.BeNil()) + + // create and deploy + commands.CreateCustomVMConfigNonSOV(subnetName, utils.SubnetEvmGenesisPath, customVMPath1) + // need to set avago version manually since VMs are custom + commands.StartNetworkWithVersion(avagoRPC1Version) + deployOutput := commands.DeploySubnetLocallyNonSOV(subnetName) + rpcs, err := utils.ParseRPCsFromOutput(deployOutput) + if err != nil { + fmt.Println(deployOutput) + } + + // check running version + // remove string suffix starting with /ext from rpc url to get node uri + nodeURI := strings.Split(rpcs[0], "/ext")[0] + vmid, err := anr_utils.VMID(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + version, err := utils.GetNodeVMVersion(nodeURI, vmid.String()) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(version).Should(gomega.Equal(subnetEVMVersion1)) + + // stop network + commands.StopNetwork() + + // upgrade + commands.UpgradeCustomVMLocal(subnetName, customVMPath2) + + // restart network + commands.StartNetworkWithVersion(avagoRPC2Version) + + // check running version + version, err = utils.GetNodeVMVersion(nodeURI, vmid.String()) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(version).Should(gomega.Equal(subnetEVMVersion2)) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can update a subnet-evm to a custom VM non SOV", func() { + customVMPath, err := utils.DownloadCustomVMBin(binaryToVersion[utils.SoloSubnetEVMKey2]) + gomega.Expect(err).Should(gomega.BeNil()) + + commands.CreateSubnetEvmConfigWithVersionNonSOV( + subnetName, + utils.SubnetEvmGenesisPath, + binaryToVersion[utils.SoloSubnetEVMKey1], + ) + + // check version + output, err := commands.DescribeSubnet(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + containsVersion1 := strings.Contains(output, binaryToVersion[utils.SoloSubnetEVMKey1]) + containsVersion2 := strings.Contains(output, binaryToVersion[utils.SoloSubnetEVMKey2]) + gomega.Expect(containsVersion1).Should(gomega.BeTrue()) + gomega.Expect(containsVersion2).Should(gomega.BeFalse()) + + _, err = commands.UpgradeCustomVM(subnetName, customVMPath) + gomega.Expect(err).Should(gomega.BeNil()) + + output, err = commands.DescribeSubnet(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + containsVersion2 = strings.Contains(output, binaryToVersion[utils.SoloSubnetEVMKey2]) + gomega.Expect(containsVersion2).Should(gomega.BeFalse()) + + // the following indicates it is a custom VM + isCustom, err := utils.IsCustomVM(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(isCustom).Should(gomega.BeTrue()) + + commands.DeleteSubnetConfig(subnetName) + }) + + ginkgo.It("can upgrade subnet-evm on public deployment non SOV", func() { + _ = commands.StartNetworkWithVersion(binaryToVersion[utils.SoloAvagoKey]) + commands.CreateSubnetEvmConfigWithVersionNonSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) + + // Simulate fuji deployment + s := commands.SimulateFujiDeployNonSOV(subnetName, keyName, controlKeys) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) + gomega.Expect(err).Should(gomega.BeNil()) + // add validators to subnet + nodeInfos, err := utils.GetNodesInfo() + gomega.Expect(err).Should(gomega.BeNil()) + for _, nodeInfo := range nodeInfos { + start := time.Now().Add(time.Second * 30).UTC().Format("2006-01-02 15:04:05") + _ = commands.SimulateFujiAddValidator(subnetName, keyName, nodeInfo.ID, start, "24h", "20") + } + // join to copy vm binary and update config file + for _, nodeInfo := range nodeInfos { + _ = commands.SimulateFujiJoin(subnetName, nodeInfo.ConfigFile, nodeInfo.PluginDir, nodeInfo.ID) + } + // get and check whitelisted subnets from config file + var whitelistedSubnets string + for _, nodeInfo := range nodeInfos { + whitelistedSubnets, err = utils.GetWhitelistedSubnetsFromConfigFile(nodeInfo.ConfigFile) + gomega.Expect(err).Should(gomega.BeNil()) + whitelistedSubnetsSlice := strings.Split(whitelistedSubnets, ",") + gomega.Expect(whitelistedSubnetsSlice).Should(gomega.ContainElement(subnetID)) + } + // update nodes whitelisted subnets + err = utils.RestartNodesWithWhitelistedSubnets(whitelistedSubnets) + gomega.Expect(err).Should(gomega.BeNil()) + // wait for subnet walidators to be up + err = utils.WaitSubnetValidators(subnetID, nodeInfos) + gomega.Expect(err).Should(gomega.BeNil()) + + var originalHash string + + // upgrade the vm on each node + vmid, err := anr_utils.VMID(subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + + for _, nodeInfo := range nodeInfos { + originalHash, err = utils.GetFileHash(filepath.Join(nodeInfo.PluginDir, vmid.String())) + gomega.Expect(err).Should(gomega.BeNil()) + } + + // stop network + commands.StopNetwork() + + for _, nodeInfo := range nodeInfos { + _, err := commands.UpgradeVMPublic(subnetName, binaryToVersion[utils.SoloSubnetEVMKey2], nodeInfo.PluginDir) + gomega.Expect(err).Should(gomega.BeNil()) + } + + for _, nodeInfo := range nodeInfos { + measuredHash, err := utils.GetFileHash(filepath.Join(nodeInfo.PluginDir, vmid.String())) + gomega.Expect(err).Should(gomega.BeNil()) + + gomega.Expect(measuredHash).ShouldNot(gomega.Equal(originalHash)) + } + + commands.DeleteSubnetConfig(subnetName) + }) +}) + +func stripWhitespaces(str string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + // if the character is a space, drop it + return -1 + } + // else keep it in the string + return r + }, str) +} diff --git a/tests/e2e/testcases/upgrade/suite.go b/tests/e2e/testcases/upgrade/sov/suite.go similarity index 85% rename from tests/e2e/testcases/upgrade/suite.go rename to tests/e2e/testcases/upgrade/sov/suite.go index fa6fe0f2a..591806076 100644 --- a/tests/e2e/testcases/upgrade/suite.go +++ b/tests/e2e/testcases/upgrade/sov/suite.go @@ -51,15 +51,15 @@ var ( ) // need to have this outside the normal suite because of the BeforeEach -var _ = ginkgo.Describe("[Upgrade expect network failure]", ginkgo.Ordered, func() { +var _ = ginkgo.Describe("[Upgrade expect network failure SOV]", ginkgo.Ordered, func() { ginkgo.AfterEach(func() { commands.CleanNetworkHard() err := utils.DeleteConfigs(subnetName) gomega.Expect(err).Should(gomega.BeNil()) }) - ginkgo.It("fails on stopped network", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("fails on stopped network SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) gomega.Expect(err).Should(gomega.BeNil()) @@ -92,15 +92,15 @@ var _ = ginkgo.Describe("[Upgrade expect network failure]", ginkgo.Ordered, func // the approach is rather simple: import the upgrade file, // call the apply command which "just" installs the file at an expected path, // and then check the file is there and has the correct content. -var _ = ginkgo.Describe("[Upgrade public network]", ginkgo.Ordered, func() { +var _ = ginkgo.Describe("[Upgrade public network SOV]", ginkgo.Ordered, func() { ginkgo.AfterEach(func() { commands.CleanNetworkHard() err := utils.DeleteConfigs(subnetName) gomega.Expect(err).Should(gomega.BeNil()) }) - ginkgo.It("can create and apply to public node", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can create and apply to public node SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) // simulate as if this had already been deployed to fuji // by just entering fake data into the struct @@ -135,7 +135,7 @@ var _ = ginkgo.Describe("[Upgrade public network]", ginkgo.Ordered, func() { // we expect the file to be present at the expected location and being // the same content as the original one - expectedPath := filepath.Join(avalanchegoConfigDir, blockchainID.String(), constants.UpgradeBytesFileName) + expectedPath := filepath.Join(avalanchegoConfigDir, blockchainID.String(), constants.UpgradeFileName) gomega.Expect(expectedPath).Should(gomega.BeARegularFile()) ori, err := os.ReadFile(upgradeBytesPath) gomega.Expect(err).Should(gomega.BeNil()) @@ -145,7 +145,7 @@ var _ = ginkgo.Describe("[Upgrade public network]", ginkgo.Ordered, func() { }) }) -var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { +var _ = ginkgo.Describe("[Upgrade local network SOV]", ginkgo.Ordered, func() { _ = ginkgo.BeforeAll(func() { mapper := utils.NewVersionMapper() binaryToVersion, err = utils.GetVersionMapping(mapper) @@ -171,8 +171,8 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { utils.DeleteCustomBinary(subnetName) }) - ginkgo.It("fails on undeployed subnet", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("fails on undeployed subnet SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) gomega.Expect(err).Should(gomega.BeNil()) @@ -184,10 +184,10 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { gomega.Expect(out).Should(gomega.ContainSubstring(upgradecmd.ErrSubnetNotDeployedOutput)) }) - ginkgo.It("can create and apply to locally running subnet", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can create and apply to locally running subnet SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) gomega.Expect(err).Should(gomega.BeNil()) @@ -218,10 +218,10 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { gomega.Expect([]byte(stripped)).Should(gomega.Equal(lockUpgradeBytes)) }) - ginkgo.It("can't upgrade transactionAllowList precompile because admin address doesn't have enough token", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can't upgrade transactionAllowList precompile because admin address doesn't have enough token SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) - commands.DeploySubnetLocally(subnetName) + commands.DeploySubnetLocallySOV(subnetName) _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath2) gomega.Expect(err).Should(gomega.BeNil()) @@ -230,10 +230,10 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.HaveOccurred()) }) - ginkgo.It("can upgrade transactionAllowList precompile because admin address has enough tokens", func() { - commands.CreateSubnetEvmConfig(subnetName, utils.SubnetEvmGenesisPath) + ginkgo.It("can upgrade transactionAllowList precompile because admin address has enough tokens SOV", func() { + commands.CreateSubnetEvmConfigSOV(subnetName, utils.SubnetEvmGenesisPath) - commands.DeploySubnetLocally(subnetName) + commands.DeploySubnetLocallySOV(subnetName) _, err = commands.ImportUpgradeBytes(subnetName, upgradeBytesPath) gomega.Expect(err).Should(gomega.BeNil()) @@ -242,10 +242,10 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) }) - ginkgo.It("can create and update future", func() { + ginkgo.It("can create and update future SOV", func() { subnetEVMVersion1 := binaryToVersion[utils.SoloSubnetEVMKey1] subnetEVMVersion2 := binaryToVersion[utils.SoloSubnetEVMKey2] - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) // check version output, err := commands.DescribeSubnet(subnetName) @@ -270,9 +270,9 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("upgrade SubnetEVM local deployment", func() { - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) - deployOutput := commands.DeploySubnetLocally(subnetName) + ginkgo.It("upgrade SubnetEVM local deployment SOV", func() { + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, subnetEVMVersion1) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -304,7 +304,7 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("upgrade custom vm local deployment", func() { + ginkgo.It("upgrade custom vm local deployment SOV", func() { // download vm bins customVMPath1, err := utils.DownloadCustomVMBin(subnetEVMVersion1) gomega.Expect(err).Should(gomega.BeNil()) @@ -312,10 +312,10 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { gomega.Expect(err).Should(gomega.BeNil()) // create and deploy - commands.CreateCustomVMConfig(subnetName, utils.SubnetEvmGenesisPath, customVMPath1) + commands.CreateCustomVMConfigSOV(subnetName, utils.SubnetEvmGenesisPath, customVMPath1) // need to set avago version manually since VMs are custom commands.StartNetworkWithVersion(avagoRPC1Version) - deployOutput := commands.DeploySubnetLocally(subnetName) + deployOutput := commands.DeploySubnetLocallySOV(subnetName) rpcs, err := utils.ParseRPCsFromOutput(deployOutput) if err != nil { fmt.Println(deployOutput) @@ -347,11 +347,11 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can update a subnet-evm to a custom VM", func() { + ginkgo.It("can update a subnet-evm to a custom VM SOV", func() { customVMPath, err := utils.DownloadCustomVMBin(binaryToVersion[utils.SoloSubnetEVMKey2]) gomega.Expect(err).Should(gomega.BeNil()) - commands.CreateSubnetEvmConfigWithVersion( + commands.CreateSubnetEvmConfigWithVersionSOV( subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1], @@ -383,13 +383,13 @@ var _ = ginkgo.Describe("[Upgrade local network]", ginkgo.Ordered, func() { commands.DeleteSubnetConfig(subnetName) }) - ginkgo.It("can upgrade subnet-evm on public deployment", func() { + ginkgo.It("can upgrade subnet-evm on public deployment SOV", func() { _ = commands.StartNetworkWithVersion(binaryToVersion[utils.SoloAvagoKey]) - commands.CreateSubnetEvmConfigWithVersion(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) + commands.CreateSubnetEvmConfigWithVersionSOV(subnetName, utils.SubnetEvmGenesisPath, binaryToVersion[utils.SoloSubnetEVMKey1]) // Simulate fuji deployment - s := commands.SimulateFujiDeploy(subnetName, keyName, controlKeys) - subnetID, err := utils.ParsePublicDeployOutput(s) + s := commands.SimulateFujiDeploySOV(subnetName, keyName, controlKeys) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() diff --git a/tests/e2e/testcases/validatormanager/suite.go b/tests/e2e/testcases/validatormanager/suite.go new file mode 100644 index 000000000..9eaa2daf4 --- /dev/null +++ b/tests/e2e/testcases/validatormanager/suite.go @@ -0,0 +1,218 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package packageman + +import ( + "encoding/hex" + "fmt" + "os" + "os/exec" + "path" + + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/key" + "github.com/ava-labs/avalanche-cli/pkg/models" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ethereum/go-ethereum/common" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + CLIBinary = "./bin/avalanche" + subnetName = "e2eSubnetTest" + keyName = "ewoq" + avalancheGoPath = "--avalanchego-path" + ewoqEVMAddress = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ewoqPChainAddress = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + testLocalNodeName = "e2eSubnetTest-local-node" +) + +var err error + +func createEtnaSubnetEvmConfig() error { + // Check config does not already exist + _, err = utils.SubnetConfigExists(subnetName) + if err != nil { + return err + } + + // Create config + cmd := exec.Command( + CLIBinary, + "blockchain", + "create", + subnetName, + "--evm", + "--proof-of-authority", + "--validator-manager-owner", + ewoqEVMAddress, + "--proxy-contract-owner", + ewoqEVMAddress, + "--production-defaults", + "--evm-chain-id=99999", + "--evm-token=TOK", + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + fmt.Println(string(output)) + return err +} + +func createSovereignSubnet() (string, string, error) { + if err := createEtnaSubnetEvmConfig(); err != nil { + return "", "", err + } + // Deploy subnet on etna devnet with local machine as bootstrap validator + cmd := exec.Command( + CLIBinary, + "blockchain", + "deploy", + subnetName, + "--etna-devnet", + "--use-local-machine", + "--num-local-nodes=1", + "--ewoq", + "--convert-only", + "--change-owner-address", + ewoqPChainAddress, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + fmt.Println(string(output)) + subnetID, err := utils.ParsePublicDeployOutput(string(output), utils.SubnetIDParseType) + if err != nil { + return "", "", err + } + blockchainID, err := utils.ParsePublicDeployOutput(string(output), utils.BlockchainIDParseType) + if err != nil { + return "", "", err + } + return subnetID, blockchainID, err +} + +func destroyLocalNode() { + _, err := os.Stat(testLocalNodeName) + if os.IsNotExist(err) { + return + } + cmd := exec.Command( + CLIBinary, + "node", + "local", + "destroy", + testLocalNodeName, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } +} + +func getBootstrapValidator() ([]*txs.ConvertSubnetToL1Validator, error) { + infoClient := info.NewClient("http://127.0.0.1:9650") + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + nodeID, proofOfPossession, err := infoClient.GetNodeID(ctx) + if err != nil { + return nil, err + } + publicKey := "0x" + hex.EncodeToString(proofOfPossession.PublicKey[:]) + pop := "0x" + hex.EncodeToString(proofOfPossession.ProofOfPossession[:]) + + bootstrapValidator := models.SubnetValidator{ + NodeID: nodeID.String(), + Weight: constants.BootstrapValidatorWeight, + Balance: constants.BootstrapValidatorBalance, + BLSPublicKey: publicKey, + BLSProofOfPossession: pop, + ChangeOwnerAddr: ewoqPChainAddress, + } + avaGoBootstrapValidators, err := blockchaincmd.ConvertToAvalancheGoSubnetValidator([]models.SubnetValidator{bootstrapValidator}) + if err != nil { + return nil, err + } + + return avaGoBootstrapValidators, nil +} + +var _ = ginkgo.Describe("[Validator Manager POA Set Up]", ginkgo.Ordered, func() { + ginkgo.BeforeEach(func() { + // key + _ = utils.DeleteKey(keyName) + output, err := commands.CreateKeyFromPath(keyName, utils.EwoqKeyPath) + if err != nil { + fmt.Println(output) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + // subnet config + _ = utils.DeleteConfigs(subnetName) + destroyLocalNode() + }) + + ginkgo.AfterEach(func() { + destroyLocalNode() + commands.DeleteSubnetConfig(subnetName) + err := utils.DeleteKey(keyName) + gomega.Expect(err).Should(gomega.BeNil()) + commands.CleanNetwork() + }) + ginkgo.It("Set Up POA Validator Manager", func() { + subnetIDStr, blockchainIDStr, err := createSovereignSubnet() + gomega.Expect(err).Should(gomega.BeNil()) + _, err = commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + keyPath := path.Join(utils.GetBaseDir(), constants.KeyDir, fmt.Sprintf("subnet_%s_airdrop", subnetName)+constants.KeySuffix) + k, err := key.LoadSoft(models.NewLocalNetwork().ID, keyPath) + gomega.Expect(err).Should(gomega.BeNil()) + rpcURL := fmt.Sprintf("http://127.0.0.1:9650/ext/bc/%s/rpc", blockchainIDStr) + client, err := evm.GetClient(rpcURL) + gomega.Expect(err).Should(gomega.BeNil()) + evm.WaitForChainID(client) + + network := models.NewNetworkFromCluster(models.NewEtnaDevnetNetwork(), testLocalNodeName) + extraAggregatorPeers, err := blockchaincmd.ConvertURIToPeers([]string{"http://127.0.0.1:9650"}) + gomega.Expect(err).Should(gomega.BeNil()) + + subnetID, err := ids.FromString(subnetIDStr) + gomega.Expect(err).Should(gomega.BeNil()) + + blockchainID, err := ids.FromString(blockchainIDStr) + gomega.Expect(err).Should(gomega.BeNil()) + + avaGoBootstrapValidators, err := getBootstrapValidator() + gomega.Expect(err).Should(gomega.BeNil()) + ownerAddress := common.HexToAddress(ewoqEVMAddress) + subnetSDK := blockchainSDK.Subnet{ + SubnetID: subnetID, + BlockchainID: blockchainID, + OwnerAddress: &ownerAddress, + RPC: rpcURL, + BootstrapValidators: avaGoBootstrapValidators, + } + + err = subnetSDK.InitializeProofOfAuthority(network, k.PrivKeyHex(), extraAggregatorPeers, logging.Off) + gomega.Expect(err).Should(gomega.BeNil()) + }) +}) diff --git a/tests/e2e/utils/constants.go b/tests/e2e/utils/constants.go index 690eccb6d..bb679c557 100644 --- a/tests/e2e/utils/constants.go +++ b/tests/e2e/utils/constants.go @@ -28,9 +28,11 @@ const ( EwoqKeyPath = "tests/e2e/assets/ewoq_key.pk" SubnetEvmAllowFeeRecpPath = "tests/e2e/assets/test_subnet_evm_allowFeeRecps_genesis.json" SubnetEvmGenesisBadPath = "tests/e2e/assets/test_subnet_evm_genesis_bad.json" + BootstrapValidatorPath = "tests/e2e/assets/test_bootstrap_validator.json" + PluginDirExt = "plugins" - PluginDirExt = "plugins" - - ledgerSimDir = "./tests/e2e/ledgerSim" - basicLedgerSimScript = "./launchAndApproveTxs.ts" + ledgerSimDir = "./tests/e2e/ledgerSim" + basicLedgerSimScript = "./launchAndApproveTxs.ts" + SubnetIDParseType = "SubnetID" + BlockchainIDParseType = "BlockchainID" ) diff --git a/tests/e2e/utils/helpers.go b/tests/e2e/utils/helpers.go index 9f3ac0126..a31d484d7 100644 --- a/tests/e2e/utils/helpers.go +++ b/tests/e2e/utils/helpers.go @@ -20,9 +20,6 @@ import ( "strings" "time" - "github.com/ava-labs/avalanchego/genesis" - "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanche-cli/pkg/binutils" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/key" @@ -31,10 +28,12 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-network-runner/client" "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" ledger "github.com/ava-labs/avalanchego/utils/crypto/ledger" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -326,7 +325,6 @@ func ParseRPCsFromOutput(output string) ([]string, error) { blockchainIDs := map[string]struct{}{} // split output by newline lines := strings.Split(output, "\n") - i := 0 for _, line := range lines { if !strings.Contains(line, "rpc") { continue @@ -335,11 +333,6 @@ func ParseRPCsFromOutput(output string) ([]string, error) { if startIndex == -1 { return nil, fmt.Errorf("no url in RPC URL line: %s", line) } - i++ - if i%2 == 1 { - // only returning blockchainID based RPCs, not aliased - continue - } endIndex := strings.Index(line, "rpc") rpc := line[startIndex : endIndex+3] rpcComponents := strings.Split(rpc, "/") @@ -358,7 +351,7 @@ func ParseRPCsFromOutput(output string) ([]string, error) { } } if len(rpcs) == 0 { - return nil, errors.New("no RPCs where found") + return nil, errors.New("no RPCs were found") } return rpcs, nil } @@ -612,25 +605,33 @@ func DownloadCustomVMBin(subnetEVMversion string) (string, error) { return subnetEVMBin, nil } -func ParsePublicDeployOutput(output string) (string, error) { +// ParsePublicDeployOutput can parse Subnet ID or Blockchain ID +func ParsePublicDeployOutput(output string, parseType string) (string, error) { lines := strings.Split(output, "\n") - var subnetID string + var targetID string for _, line := range lines { - if !strings.Contains(line, "Subnet ID") && !strings.Contains(line, "RPC URL") { + if !strings.Contains(line, "Subnet ID") && !strings.Contains(line, "RPC URL") && !strings.Contains(line, " Blockchain ID") { continue } words := strings.Split(line, "|") if len(words) != 4 { return "", errors.New("error parsing output: invalid number of words in line") } - if strings.Contains(line, "Subnet ID") { - subnetID = strings.TrimSpace(words[2]) + if parseType == SubnetIDParseType { + if strings.Contains(line, "Subnet ID") { + targetID = strings.TrimSpace(words[2]) + } + } + if parseType == BlockchainIDParseType { + if strings.Contains(line, "Blockchain ID") { + targetID = strings.TrimSpace(words[2]) + } } } - if subnetID == "" { + if targetID == "" { return "", errors.New("information not found in output") } - return subnetID, nil + return targetID, nil } func RestartNodesWithWhitelistedSubnets(whitelistedSubnets string) error { @@ -652,7 +653,7 @@ func RestartNodesWithWhitelistedSubnets(whitelistedSubnets string) error { return err } } - ctx, cancel = utils.GetAPIContext() + ctx, cancel = utils.GetANRContext() _, err = cli.Health(ctx) cancel() if err != nil { @@ -1066,3 +1067,7 @@ func GetKeyTransferFee(output string) (uint64, error) { } return feeNAvax, nil } + +func GetAPILargeContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), constants.APIRequestLargeTimeout) +}