diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index 37ab982fb..8b34b2f03 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -81,6 +81,7 @@ var ( avagoBinaryPath string numBootstrapValidators int numLocalNodes int + partialSync bool changeOwnerAddress string subnetOnly bool icmSpec subnet.ICMSpec @@ -151,6 +152,7 @@ so you can take your locally tested Subnet and deploy it on Fuji or Mainnet.`, 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().BoolVar(&partialSync, "partial-sync", true, "set primary network partial sync for new validators") return cmd } @@ -534,13 +536,12 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { return err } } - nodeConfig := "" + nodeConfig := map[string]interface{}{} if app.AvagoNodeConfigExists(blockchainName) { - nodeConfigBytes, err := os.ReadFile(app.GetAvagoNodeConfigPath(blockchainName)) + nodeConfig, err = utils.ReadJSON(app.GetAvagoNodeConfigPath(blockchainName)) if err != nil { return err } - nodeConfig = string(nodeConfigBytes) } // anrSettings, avagoVersionSettings, globalNetworkFlags are empty if err = node.StartLocalNode( @@ -549,6 +550,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { useEtnaDevnet, avagoBinaryPath, uint32(numLocalNodes), + partialSync, nodeConfig, anrSettings, avagoVersionSettings, diff --git a/cmd/nodecmd/create.go b/cmd/nodecmd/create.go index aa3e589e1..fd0fab29c 100644 --- a/cmd/nodecmd/create.go +++ b/cmd/nodecmd/create.go @@ -129,6 +129,7 @@ will apply to all nodes in the cluster`, 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 } @@ -231,6 +232,10 @@ func preCreateChecks(clusterName string) error { 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 } @@ -790,10 +795,12 @@ func createNodes(cmd *cobra.Command, args []string) error { avalancheGoVersion, bootstrapIDs, bootstrapIPs, + partialSync, genesisPath, upgradePath, addMonitoring, - publicAccessToHTTPPort); err != nil { + publicAccessToHTTPPort, + ); err != nil { nodeResults.AddResult(host.NodeID, nil, err) ux.SpinFailWithError(spinner, "", err) return diff --git a/cmd/nodecmd/local.go b/cmd/nodecmd/local.go index a8239971c..2ba631349 100644 --- a/cmd/nodecmd/local.go +++ b/cmd/nodecmd/local.go @@ -4,11 +4,11 @@ package nodecmd import ( "fmt" - "os" "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" @@ -26,6 +26,7 @@ var ( stakingSignerKeyPath string numNodes uint32 nodeConfigPath string + partialSync bool ) // const snapshotName = "local_snapshot" @@ -84,6 +85,7 @@ status by running avalanche node status local 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 } @@ -151,20 +153,24 @@ func localStartNode(_ *cobra.Command, args []string) error { UseLatestAvalanchegoReleaseVersion: useLatestAvalanchegoReleaseVersion, UseAvalanchegoVersionFromSubnet: useAvalanchegoVersionFromSubnet, } - nodeConfig := "" + var ( + err error + nodeConfig map[string]interface{} + ) if nodeConfigPath != "" { - nodeConfigBytes, err := os.ReadFile(nodeConfigPath) + nodeConfig, err = utils.ReadJSON(nodeConfigPath) if err != nil { return err } - nodeConfig = string(nodeConfigBytes) } + return node.StartLocalNode( app, clusterName, globalNetworkFlags.UseEtnaDevnet, avalanchegoBinaryPath, numNodes, + partialSync, nodeConfig, anrSettings, avaGoVersionSetting, diff --git a/cmd/root.go b/cmd/root.go index 7fb694927..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" @@ -354,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/go.mod b/go.mod index e82fd9a50..055a3a164 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.8 require ( github.com/ava-labs/apm v1.0.0 - github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241023180457-5189aac811fb + github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241113212913-591353070499 github.com/ava-labs/avalanchego v1.12.0-initial-poc.6 github.com/ava-labs/awm-relayer v1.4.1-0.20241101130521-c20945eebe03 github.com/ava-labs/coreth v0.13.8 diff --git a/go.sum b/go.sum index 7029947dc..53e4a4b39 100644 --- a/go.sum +++ b/go.sum @@ -83,8 +83,12 @@ 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.20241023180457-5189aac811fb h1:Fv5gyTIERypRVxtB4JSNoe16o45P0THySRDfrTuX0Dc= -github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241023180457-5189aac811fb/go.mod h1:ASWB/CKJm8wVZUBp3DY0AV8jTBLCs01kzQz9GGwtzi8= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241101154041-1be2d617875e h1:zafSQnLPTu4asqqrwg0fhSYUfOQ72VlmHzDfVOMXSsE= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241101154041-1be2d617875e/go.mod h1:ASWB/CKJm8wVZUBp3DY0AV8jTBLCs01kzQz9GGwtzi8= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241102130338-5a17aecf300c h1:KhTnFwU5i7HCkygPphAoj9ldFmaAqLi2diZBHilQGOQ= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241102130338-5a17aecf300c/go.mod h1:ASWB/CKJm8wVZUBp3DY0AV8jTBLCs01kzQz9GGwtzi8= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241113212913-591353070499 h1:aSsL/Kxb0R/YJljJw9WFoQIA7yREFDqJxupPzCR75d4= +github.com/ava-labs/avalanche-network-runner v1.8.4-0.20241113212913-591353070499/go.mod h1:ASWB/CKJm8wVZUBp3DY0AV8jTBLCs01kzQz9GGwtzi8= github.com/ava-labs/avalanchego v1.12.0-initial-poc.6 h1:7Ijm0POq/NGX6jQG08BlOPfuHi9JYtDy4HylNAjel2A= github.com/ava-labs/avalanchego v1.12.0-initial-poc.6/go.mod h1:gYlTU42Q4b29hzhUN22yclym5qwB3Si0jh4+LTn7DZM= github.com/ava-labs/awm-relayer v1.4.1-0.20241101130521-c20945eebe03 h1:f2g8uDwIggTcm8zxkCh+rA9r/xt9IvSlpbGhFJULaKQ= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index a2427507e..994c9375f 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -59,6 +59,8 @@ const ( APIRequestLargeTimeout = 5 * time.Second FastGRPCDialTimeout = 100 * time.Millisecond + FujiBootstrapTimeout = 5 * time.Minute + SSHServerStartTimeout = 1 * time.Minute SSHScriptTimeout = 2 * time.Minute SSHLongRunningScriptTimeout = 10 * time.Minute diff --git a/pkg/docker/config.go b/pkg/docker/config.go index 16c26f827..8a8a303c9 100644 --- a/pkg/docker/config.go +++ b/pkg/docker/config.go @@ -17,6 +17,7 @@ import ( type AvalancheGoConfigOptions struct { BootstrapIPs []string BootstrapIDs []string + PartialSync bool GenesisPath string UpgradePath string AllowPublicAccess bool @@ -31,6 +32,7 @@ func prepareAvalanchegoConfig( 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 != "" { diff --git a/pkg/docker/ssh.go b/pkg/docker/ssh.go index 7c7b83223..aab8a089c 100644 --- a/pkg/docker/ssh.go +++ b/pkg/docker/ssh.go @@ -31,6 +31,7 @@ func ComposeSSHSetupNode( avalancheGoVersion string, avalanchegoBootstrapIDs []string, avalanchegoBootstrapIPs []string, + partialSync bool, avalanchegoGenesisFilePath string, avalanchegoUpgradeFilePath string, withMonitoring bool, @@ -57,6 +58,7 @@ func ComposeSSHSetupNode( AvalancheGoConfigOptions{ BootstrapIDs: avalanchegoBootstrapIDs, BootstrapIPs: avalanchegoBootstrapIPs, + PartialSync: partialSync, GenesisPath: avalanchegoGenesisFilePath, UpgradePath: avalanchegoUpgradeFilePath, AllowPublicAccess: publicAccessToHTTPPort, diff --git a/pkg/models/network.go b/pkg/models/network.go index f0bdc1306..04db90794 100644 --- a/pkg/models/network.go +++ b/pkg/models/network.go @@ -3,6 +3,7 @@ package models import ( + "context" "fmt" "os" "strings" @@ -224,6 +225,15 @@ 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 diff --git a/pkg/node/local.go b/pkg/node/local.go index 7ecebdbb2..e94674d9e 100644 --- a/pkg/node/local.go +++ b/pkg/node/local.go @@ -4,6 +4,7 @@ package node import ( "encoding/hex" + "encoding/json" "fmt" "os" "path/filepath" @@ -23,7 +24,9 @@ import ( "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" @@ -156,7 +159,8 @@ func StartLocalNode( useEtnaDevnet bool, avalanchegoBinaryPath string, numNodes uint32, - nodeConfig string, + partialSync bool, + nodeConfig map[string]interface{}, anrSettings ANRSettings, avaGoVersionSetting AvalancheGoVersionSettings, globalNetworkFlags networkoptions.NetworkFlags, @@ -197,6 +201,7 @@ func StartLocalNode( ctx, cancel := utils.GetANRContext() defer cancel() + // starts server avalancheGoVersion := "latest" if avalanchegoBinaryPath == "" { @@ -237,6 +242,18 @@ func StartLocalNode( 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{ @@ -244,7 +261,7 @@ func StartLocalNode( client.WithReassignPortsIfUsed(true), client.WithPluginDir(pluginDir), client.WithSnapshotPath(rootDir), - client.WithGlobalNodeConfig(nodeConfig), + client.WithGlobalNodeConfig(nodeConfigStr), } // load snapshot for existing network if _, err := cli.LoadSnapshot( @@ -258,14 +275,22 @@ func StartLocalNode( } else { ux.Logger.GreenCheckmarkToUser("Local cluster %s not found. Creating...", clusterName) network := models.UndefinedNetwork - if useEtnaDevnet { + switch { + case useEtnaDevnet: network = models.NewNetwork( models.Devnet, constants.EtnaDevnetNetworkID, constants.EtnaDevnetEndpoint, clusterName, ) - } else { + case globalNetworkFlags.UseFuji: + network = models.NewNetwork( + models.Fuji, + avagoconstants.FujiID, + constants.FujiAPIEndpoint, + clusterName, + ) + default: network, err = networkoptions.GetNetworkFromCmdLineFlags( app, "", @@ -337,7 +362,7 @@ func StartLocalNode( client.WithPluginDir(pluginDir), client.WithFreshStakingIds(true), client.WithZeroIP(false), - client.WithGlobalNodeConfig(nodeConfig), + client.WithGlobalNodeConfig(nodeConfigStr), } if anrSettings.GenesisPath != "" && utils.FileExists(anrSettings.GenesisPath) { anrOpts = append(anrOpts, client.WithGenesisPath(anrSettings.GenesisPath)) @@ -352,6 +377,9 @@ func StartLocalNode( 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...") diff --git a/pkg/remoteconfig/avalanche.go b/pkg/remoteconfig/avalanche.go index a51c140f9..777539fe9 100644 --- a/pkg/remoteconfig/avalanche.go +++ b/pkg/remoteconfig/avalanche.go @@ -27,6 +27,7 @@ type AvalancheConfigInputs struct { TrackSubnets string BootstrapIDs string BootstrapIPs string + PartialSync bool GenesisPath string UpgradePath string ProposerVMUseCurrentHeight bool diff --git a/pkg/remoteconfig/templates/avalanche-node.tmpl b/pkg/remoteconfig/templates/avalanche-node.tmpl index 45d7849fd..dfd9e318e 100644 --- a/pkg/remoteconfig/templates/avalanche-node.tmpl +++ b/pkg/remoteconfig/templates/avalanche-node.tmpl @@ -4,6 +4,7 @@ "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 }} diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 2339643e2..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" @@ -587,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) 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 +}