diff --git a/.gitignore b/.gitignore index e14603067..90a73d3e2 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/ diff --git a/cmd/blockchaincmd/prompt_owners.go b/cmd/blockchaincmd/prompt_owners.go index b76061675..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)) diff --git a/cmd/nodecmd/local.go b/cmd/nodecmd/local.go index e5b324729..a8239971c 100644 --- a/cmd/nodecmd/local.go +++ b/cmd/nodecmd/local.go @@ -3,6 +3,7 @@ package nodecmd import ( + "fmt" "os" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" @@ -119,13 +120,18 @@ func newLocalDestroyCmd() *cobra.Command { } func newLocalStatusCmd() *cobra.Command { - return &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 { @@ -185,7 +191,10 @@ func localStatus(_ *cobra.Command, args []string) error { if len(args) > 0 { clusterName = args[0] } - return node.LocalStatus(app, clusterName) + 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 { diff --git a/pkg/node/local.go b/pkg/node/local.go index 3bd123e3f..e3fc12a0c 100644 --- a/pkg/node/local.go +++ b/pkg/node/local.go @@ -3,10 +3,12 @@ package node import ( + "encoding/hex" "fmt" "os" "path/filepath" "slices" + "strings" "github.com/ava-labs/avalanche-cli/pkg/application" "github.com/ava-labs/avalanche-cli/pkg/binutils" @@ -20,9 +22,12 @@ import ( "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/ids" "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( @@ -510,7 +515,7 @@ func listLocalClusters(app *application.Avalanche, clusterNamesToInclude []strin return localClusters, nil } -func LocalStatus(app *application.Avalanche, clusterName string) error { +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 { @@ -529,6 +534,7 @@ func LocalStatus(app *application.Avalanche, clusterName string) error { 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 { @@ -536,6 +542,10 @@ func LocalStatus(app *application.Avalanche, clusterName string) error { 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) @@ -543,13 +553,28 @@ func LocalStatus(app *application.Avalanche, clusterName string) error { return fmt.Errorf("failed to list local clusters: %w", err) } if clusterName != "" { - ux.Logger.PrintToUser("%s %s", logging.LightBlue.Wrap("Local cluster:"), logging.Green.Wrap(clusterName)) + ux.Logger.PrintToUser("%s %s", logging.Blue.Wrap("Local cluster:"), logging.Green.Wrap(clusterName)) } else { - ux.Logger.PrintToUser(logging.LightBlue.Wrap("Local clusters:")) + 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 + } + blockchainID = sc.Networks[clusterConf.Network.Name()].BlockchainID + } if rootDir == currentlyRunningRootDir { currenlyRunning = fmt.Sprintf(" [%s]", logging.Blue.Wrap("Running")) if isHealthy { @@ -557,11 +582,76 @@ func LocalStatus(app *application.Avalanche, clusterName string) error { } 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 +}