gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/hostcmd.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"os"
     7  	"sort"
     8  	"strings"
     9  	"text/tabwriter"
    10  
    11  	"github.com/spf13/cobra"
    12  
    13  	"gitlab.com/NebulousLabs/errors"
    14  	"gitlab.com/SkynetLabs/skyd/node/api"
    15  	"gitlab.com/SkynetLabs/skyd/node/api/client"
    16  	"go.sia.tech/siad/crypto"
    17  	"go.sia.tech/siad/modules"
    18  	"go.sia.tech/siad/types"
    19  )
    20  
    21  var (
    22  	hostAnnounceCmd = &cobra.Command{
    23  		Use:   "announce",
    24  		Short: "Announce yourself as a host",
    25  		Long: `Announce yourself as a host on the network.
    26  Announcing will also configure the host to start accepting contracts.
    27  You can revert this by running:
    28  	siac host config acceptingcontracts false
    29  You may also supply a specific address to be announced, e.g.:
    30  	siac host announce my-host-domain.com:9001
    31  Doing so will override the standard connectivity checks.`,
    32  		Run: hostannouncecmd,
    33  	}
    34  
    35  	hostCmd = &cobra.Command{
    36  		Use:   "host",
    37  		Short: "Perform host actions",
    38  		Long:  "View or modify host settings.",
    39  		Run:   wrap(hostcmd),
    40  	}
    41  
    42  	hostConfigCmd = &cobra.Command{
    43  		Use:   "config [setting] [value]",
    44  		Short: "Modify host settings",
    45  		Long: `Modify host settings.
    46  
    47  Available settings:
    48       acceptingcontracts:   boolean
    49       maxduration:          blocks
    50       maxdownloadbatchsize: bytes
    51       maxrevisebatchsize:   bytes
    52       netaddress:           string
    53       windowsize:           blocks
    54  
    55       collateral:       currency
    56       collateralbudget: currency
    57       maxcollateral:    currency
    58  
    59       minbaserpcprice:           currency
    60       mincontractprice:          currency
    61       mindownloadbandwidthprice: currency / TB
    62       minsectoraccessprice:      currency
    63       minstorageprice:           currency / TB / Month
    64       minuploadbandwidthprice:   currency / TB
    65  
    66       ephemeralaccountexpiry:     seconds
    67       maxephemeralaccountbalance: currency
    68       maxephemeralaccountrisk:    currency
    69  	 
    70       registrysize:       filesize
    71       customregistrypath: string
    72  
    73  Currency units can be specified, e.g. 10SC; run 'skyc help wallet' for details.
    74  
    75  Durations (maxduration and windowsize) must be specified in either blocks (b),
    76  hours (h), days (d), or weeks (w). A block is approximately 10 minutes, so one
    77  hour is six blocks, a day is 144 blocks, and a week is 1008 blocks.
    78  
    79  Timeouts (ephemeralaccountexpiry) must be specified in either seconds (s),
    80  hours (h), days (d), or weeks (w). One hour is 3600 seconds, a day is 86400
    81  seconds, and a week is 604800 seconds.
    82  
    83  For a description of each parameter, see doc/API.md.
    84  
    85  To configure the host to accept new contracts, set acceptingcontracts to true:
    86  	siac host config acceptingcontracts true
    87  `,
    88  		Run: wrap(hostconfigcmd),
    89  	}
    90  
    91  	hostContractCmd = &cobra.Command{
    92  		Use:   "contracts",
    93  		Short: "Show host contracts",
    94  		Long: `Show host contracts sorted by expiration height.
    95  
    96  Available output types:
    97       value:  show financial information
    98       status: show status information
    99  `,
   100  		Run: wrap(hostcontractcmd),
   101  	}
   102  
   103  	hostFolderAddCmd = &cobra.Command{
   104  		Use:   "add [path] [size]",
   105  		Short: "Add a storage folder to the host",
   106  		Long:  "Add a storage folder to the host, specifying how much data it should store",
   107  		Run:   wrap(hostfolderaddcmd),
   108  	}
   109  
   110  	hostFolderCmd = &cobra.Command{
   111  		Use:   "folder",
   112  		Short: "Add, remove, or resize a storage folder",
   113  		Long:  "Add, remove, or resize a storage folder.",
   114  	}
   115  
   116  	hostFolderRemoveCmd = &cobra.Command{
   117  		Use:   "remove [path]",
   118  		Short: "Remove a storage folder from the host",
   119  		Long: `Remove a storage folder from the host. Note that this does not delete any
   120  data; it will instead be distributed across the remaining storage folders unless the force flag is used.`,
   121  
   122  		Run: wrap(hostfolderremovecmd),
   123  	}
   124  
   125  	hostFolderResizeCmd = &cobra.Command{
   126  		Use:   "resize [path] [size]",
   127  		Short: "Resize a storage folder",
   128  		Long: `Change how much data a storage folder should store. If the new size is less
   129  than what the folder is currently storing, data will be distributed across the
   130  other storage folders.`,
   131  		Run: wrap(hostfolderresizecmd),
   132  	}
   133  
   134  	hostSectorCmd = &cobra.Command{
   135  		Use:   "sector",
   136  		Short: "Add or delete a sector (add not supported)",
   137  		Long: `Add or delete a sector. Adding is not currently supported. Note that
   138  deleting a sector may impact host revenue.`,
   139  	}
   140  
   141  	hostSectorDeleteCmd = &cobra.Command{
   142  		Use:   "delete [root]",
   143  		Short: "Delete a sector",
   144  		Long: `Delete a sector, identified by its Merkle root. Note that deleting a
   145  sector may impact host revenue.`,
   146  		Run: wrap(hostsectordeletecmd),
   147  	}
   148  )
   149  
   150  // hostcmd is the handler for the command `skyc host`.
   151  // Prints info about the host and its storage folders.
   152  func hostcmd() {
   153  	hg, err := httpClient.HostGet()
   154  	if errors.Contains(err, api.ErrAPICallNotRecognized) {
   155  		// Assume module is not loaded if status command is not recognized.
   156  		fmt.Printf("Host:\n  Status: %s\n\n", moduleNotReadyStatus)
   157  		return
   158  	} else if err != nil {
   159  		die("Could not fetch host settings:", err)
   160  	}
   161  
   162  	sg, err := httpClient.HostStorageGet()
   163  	if err != nil {
   164  		die("Could not fetch storage info:", err)
   165  	}
   166  
   167  	es := hg.ExternalSettings
   168  	fm := hg.FinancialMetrics
   169  	is := hg.InternalSettings
   170  	nm := hg.NetworkMetrics
   171  
   172  	// calculate total storage available and remaining
   173  	var totalstorage, storageremaining uint64
   174  	for _, folder := range sg.Folders {
   175  		totalstorage += folder.Capacity
   176  		storageremaining += folder.CapacityRemaining
   177  	}
   178  
   179  	// convert price from bytes/block to TB/Month
   180  	price := currencyUnits(is.MinStoragePrice.Mul(modules.BlockBytesPerMonthTerabyte))
   181  	// calculate total revenue
   182  	totalRevenue := fm.ContractCompensation.
   183  		Add(fm.StorageRevenue).
   184  		Add(fm.DownloadBandwidthRevenue).
   185  		Add(fm.UploadBandwidthRevenue)
   186  	totalPotentialRevenue := fm.PotentialContractCompensation.
   187  		Add(fm.PotentialStorageRevenue).
   188  		Add(fm.PotentialDownloadBandwidthRevenue).
   189  		Add(fm.PotentialUploadBandwidthRevenue)
   190  	// determine the display method for the net address.
   191  	netaddr := es.NetAddress
   192  	if is.NetAddress == "" {
   193  		netaddr += " (automatically determined)"
   194  	} else {
   195  		netaddr += " (manually specified)"
   196  	}
   197  
   198  	var connectabilityString string
   199  	if hg.WorkingStatus == "working" {
   200  		connectabilityString = "Host appears to be working."
   201  	} else if hg.WorkingStatus == "not working" && hg.ConnectabilityStatus == "connectable" {
   202  		connectabilityString = "Nobody is connecting to host. Try re-announcing."
   203  	} else if hg.WorkingStatus == "checking" || hg.ConnectabilityStatus == "checking" {
   204  		connectabilityString = "Host is checking status (takes a few minutes)."
   205  	} else {
   206  		connectabilityString = "Host is not connectable (re-checks every few minutes)."
   207  	}
   208  
   209  	if verbose {
   210  		// describe net address
   211  		fmt.Printf(`General Info:
   212  	Connectability Status: %v
   213  	Version:               %v
   214  
   215  Host Internal Settings:
   216  	acceptingcontracts:   %v
   217  	maxdownloadbatchsize: %v
   218  	maxduration:          %v Weeks
   219  	maxrevisebatchsize:   %v
   220  	netaddress:           %v
   221  	windowsize:           %v Hours
   222  
   223  	collateral:       %v / TB / Month
   224  	collateralbudget: %v
   225  	maxcollateral:    %v Per Contract
   226  
   227  	minbaserpcprice:           %v
   228  	mincontractprice:          %v
   229  	mindownloadbandwidthprice: %v / TB
   230  	minsectoraccessprice:      %v
   231  	minstorageprice:           %v / TB / Month
   232  	minuploadbandwidthprice:   %v / TB
   233  
   234  	ephemeralaccountexpiry:     %vs
   235  	maxephemeralaccountbalance: %v
   236  	maxephemeralaccountrisk:    %v
   237  
   238  	registrysize:       %v
   239  	customregistrypath: %v
   240  
   241  Host Financials:
   242  	Contract Count:               %v
   243  	Transaction Fee Compensation: %v
   244  	Potential Fee Compensation:   %v
   245  	Transaction Fee Expenses:     %v
   246  
   247  	Storage Revenue:           %v
   248  	Potential Storage Revenue: %v
   249  
   250  	Locked Collateral: %v
   251  	Risked Collateral: %v
   252  	Lost Collateral:   %v
   253  
   254  	Download Revenue:           %v
   255  	Potential Download Revenue: %v
   256  	Upload Revenue:             %v
   257  	Potential Upload Revenue:   %v
   258  
   259  RPC Stats:
   260  	Error Calls:        %v
   261  	Unrecognized Calls: %v
   262  	Download Calls:     %v
   263  	Renew Calls:        %v
   264  	Revise Calls:       %v
   265  	Settings Calls:     %v
   266  	FormContract Calls: %v
   267  `,
   268  			connectabilityString,
   269  			es.Version,
   270  
   271  			yesNo(is.AcceptingContracts),
   272  			modules.FilesizeUnits(is.MaxDownloadBatchSize),
   273  			periodUnits(is.MaxDuration),
   274  			modules.FilesizeUnits(is.MaxReviseBatchSize),
   275  			netaddr,
   276  			is.WindowSize/6,
   277  
   278  			currencyUnits(is.Collateral.Mul(modules.BlockBytesPerMonthTerabyte)),
   279  			currencyUnits(is.CollateralBudget),
   280  			currencyUnits(is.MaxCollateral),
   281  
   282  			currencyUnits(is.MinBaseRPCPrice),
   283  			currencyUnits(is.MinContractPrice),
   284  			currencyUnits(is.MinDownloadBandwidthPrice.Mul(modules.BytesPerTerabyte)),
   285  			currencyUnits(is.MinSectorAccessPrice),
   286  			currencyUnits(is.MinStoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)),
   287  			currencyUnits(is.MinUploadBandwidthPrice.Mul(modules.BytesPerTerabyte)),
   288  
   289  			is.EphemeralAccountExpiry.Seconds(),
   290  			currencyUnits(is.MaxEphemeralAccountBalance),
   291  			currencyUnits(is.MaxEphemeralAccountRisk),
   292  			modules.FilesizeUnits(is.RegistrySize),
   293  			is.CustomRegistryPath,
   294  
   295  			fm.ContractCount, currencyUnits(fm.ContractCompensation),
   296  			currencyUnits(fm.PotentialContractCompensation),
   297  			currencyUnits(fm.TransactionFeeExpenses),
   298  
   299  			currencyUnits(fm.StorageRevenue),
   300  			currencyUnits(fm.PotentialStorageRevenue),
   301  
   302  			currencyUnits(fm.LockedStorageCollateral),
   303  			currencyUnits(fm.RiskedStorageCollateral),
   304  			currencyUnits(fm.LostStorageCollateral),
   305  
   306  			currencyUnits(fm.DownloadBandwidthRevenue),
   307  			currencyUnits(fm.PotentialDownloadBandwidthRevenue),
   308  			currencyUnits(fm.UploadBandwidthRevenue),
   309  			currencyUnits(fm.PotentialUploadBandwidthRevenue),
   310  
   311  			nm.ErrorCalls, nm.UnrecognizedCalls, nm.DownloadCalls,
   312  			nm.RenewCalls, nm.ReviseCalls, nm.SettingsCalls,
   313  			nm.FormContractCalls)
   314  	} else {
   315  		fmt.Printf(`Host info:
   316  	Connectability Status: %v
   317  
   318  	Storage:      %v (%v used)
   319  	Price:        %v / TB / Month
   320  	Max Duration: %v Weeks
   321  
   322  	Accepting Contracts:  %v
   323  	Anticipated Revenue:  %v
   324  	Locked Collateral:    %v
   325  	Revenue:              %v
   326  `,
   327  			connectabilityString,
   328  
   329  			modules.FilesizeUnits(totalstorage),
   330  			modules.FilesizeUnits(totalstorage-storageremaining), price,
   331  			periodUnits(is.MaxDuration),
   332  
   333  			yesNo(is.AcceptingContracts), currencyUnits(totalPotentialRevenue),
   334  			currencyUnits(fm.LockedStorageCollateral),
   335  			currencyUnits(totalRevenue))
   336  	}
   337  
   338  	// if wallet is locked print warning
   339  	walletstatus, walleterr := httpClient.WalletGet()
   340  	if walleterr != nil {
   341  		fmt.Print("\nWarning:\n	Could not get wallet status. A working wallet is needed in order to operate your host. Error: ")
   342  		fmt.Println(walleterr)
   343  	} else if !walletstatus.Unlocked {
   344  		fmt.Println("\nWarning:\n	Your wallet is locked. You must unlock your wallet for the host to function properly.")
   345  	}
   346  
   347  	fmt.Println("\nStorage Folders:")
   348  
   349  	// display storage folder info
   350  	sort.Slice(sg.Folders, func(i, j int) bool {
   351  		return sg.Folders[i].Path < sg.Folders[j].Path
   352  	})
   353  	if len(sg.Folders) == 0 {
   354  		fmt.Println("No storage folders configured")
   355  		return
   356  	}
   357  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
   358  	fmt.Fprintf(w, "\tUsed\tCapacity\t%% Used\tPath\n")
   359  	for _, folder := range sg.Folders {
   360  		curSize := int64(folder.Capacity - folder.CapacityRemaining)
   361  		pctUsed := 100 * (float64(curSize) / float64(folder.Capacity))
   362  		fmt.Fprintf(w, "\t%s\t%s\t%.2f\t%s\n", modules.FilesizeUnits(uint64(curSize)), modules.FilesizeUnits(folder.Capacity), pctUsed, folder.Path)
   363  	}
   364  	if err := w.Flush(); err != nil {
   365  		die("failed to flush writer")
   366  	}
   367  }
   368  
   369  // hostconfigcmd is the handler for the command `skyc host config [setting] [value]`.
   370  // Modifies host settings.
   371  func hostconfigcmd(param, value string) {
   372  	var err error
   373  	switch param {
   374  	// currency (convert to hastings)
   375  	case "collateralbudget", "maxcollateral", "minbaserpcprice", "mincontractprice", "minsectoraccessprice", "maxephemeralaccountbalance", "maxephemeralaccountrisk":
   376  		value, err = types.ParseCurrency(value)
   377  		if err != nil {
   378  			die("Could not parse "+param+":", err)
   379  		}
   380  
   381  	// currency/TB (convert to hastings/byte)
   382  	case "mindownloadbandwidthprice", "minuploadbandwidthprice":
   383  		hastings, err := types.ParseCurrency(value)
   384  		if err != nil {
   385  			die("Could not parse "+param+":", err)
   386  		}
   387  		i, _ := new(big.Int).SetString(hastings, 10)
   388  		c := types.NewCurrency(i).Div(modules.BytesPerTerabyte)
   389  		value = c.String()
   390  
   391  	// currency/TB/month (convert to hastings/byte/block)
   392  	case "collateral", "minstorageprice":
   393  		hastings, err := types.ParseCurrency(value)
   394  		if err != nil {
   395  			die("Could not parse "+param+":", err)
   396  		}
   397  		i, _ := new(big.Int).SetString(hastings, 10)
   398  		c := types.NewCurrency(i).Div(modules.BlockBytesPerMonthTerabyte)
   399  		value = c.String()
   400  
   401  	// bool (allow "yes" and "no")
   402  	case "acceptingcontracts":
   403  		switch strings.ToLower(value) {
   404  		case "yes":
   405  			value = "true"
   406  		case "no":
   407  			value = "false"
   408  		}
   409  
   410  	// duration (convert to blocks)
   411  	case "maxduration", "windowsize":
   412  		value, err = parsePeriod(value)
   413  		if err != nil {
   414  			die("Could not parse "+param+":", err)
   415  		}
   416  
   417  	// filesize (convert to bytes)
   418  	case "registrysize":
   419  		value, err = parseFilesize(value)
   420  		if err != nil {
   421  			die("Could not parse "+param+":", err)
   422  		}
   423  
   424  	// timeout (convert to seconds)
   425  	case "ephemeralaccountexpiry":
   426  		value, err = parseTimeout(value)
   427  		if err != nil {
   428  			die("Could not parse "+param+":", err)
   429  		}
   430  
   431  	// other valid settings
   432  	case "maxdownloadbatchsize", "maxrevisebatchsize", "netaddress", "customregistrypath":
   433  
   434  	// invalid settings
   435  	default:
   436  		die("\"" + param + "\" is not a host setting")
   437  	}
   438  	err = httpClient.HostModifySettingPost(client.HostParam(param), value)
   439  	if err != nil {
   440  		die("Failed to update host settings:", err)
   441  	}
   442  	fmt.Println("Host settings updated.")
   443  
   444  	// get the estimated conversion rate.
   445  	eg, err := httpClient.HostEstimateScoreGet(param, value)
   446  	if err != nil {
   447  		if err.Error() == "cannot call /host/estimatescore without the renter module" {
   448  			// score estimate requires the renter module
   449  			return
   450  		}
   451  		die("could not get host score estimate:", err)
   452  	}
   453  	fmt.Printf("Estimated conversion rate: %v%%\n", eg.ConversionRate)
   454  }
   455  
   456  // hostcontractcmd is the handler for the command `skyc host contracts [type]`.
   457  func hostcontractcmd() {
   458  	cg, err := httpClient.HostContractInfoGet()
   459  	if err != nil {
   460  		die("Could not fetch host contract info:", err)
   461  	}
   462  	sort.Slice(cg.Contracts, func(i, j int) bool { return cg.Contracts[i].ExpirationHeight < cg.Contracts[j].ExpirationHeight })
   463  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
   464  	switch hostContractOutputType {
   465  	case "value":
   466  		fmt.Fprintf(w, "Obligation Id\tObligation Status\tContract Cost\tLocked Collateral\tRisked Collateral\tPotential Revenue\tExpiration Height\tTransaction Fees\n")
   467  		for _, so := range cg.Contracts {
   468  			potentialRevenue := so.PotentialDownloadRevenue.Add(so.PotentialUploadRevenue).Add(so.PotentialStorageRevenue).Add(so.PotentialAccountFunding)
   469  			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s\n", so.ObligationId, strings.TrimPrefix(so.ObligationStatus, "obligation"), currencyUnits(so.ContractCost), currencyUnits(so.LockedCollateral),
   470  				currencyUnits(so.RiskedCollateral), currencyUnits(potentialRevenue), so.ExpirationHeight, currencyUnits(so.TransactionFeesAdded))
   471  		}
   472  	case "status":
   473  		fmt.Fprintf(w, "Obligation ID\tObligation Status\tExpiration Height\tOrigin Confirmed\tRevision Constructed\tRevision Confirmed\tProof Constructed\tProof Confirmed\n")
   474  		for _, so := range cg.Contracts {
   475  			fmt.Fprintf(w, "%s\t%s\t%d\t%t\t%t\t%t\t%t\t%t\n", so.ObligationId, strings.TrimPrefix(so.ObligationStatus, "obligation"), so.ExpirationHeight, so.OriginConfirmed,
   476  				so.RevisionConstructed, so.RevisionConfirmed, so.ProofConstructed, so.ProofConfirmed)
   477  		}
   478  	default:
   479  		die("\"" + hostContractOutputType + "\" is not a format")
   480  	}
   481  	if err := w.Flush(); err != nil {
   482  		die("failed to flush writer")
   483  	}
   484  }
   485  
   486  // hostannouncecmd is the handler for the command `skyc host announce`.
   487  // Announces yourself as a host to the network. Optionally takes an address to
   488  // announce as.
   489  func hostannouncecmd(cmd *cobra.Command, args []string) {
   490  	var err error
   491  	switch len(args) {
   492  	case 0:
   493  		err = httpClient.HostAnnouncePost()
   494  	case 1:
   495  		err = httpClient.HostAnnounceAddrPost(modules.NetAddress(args[0]))
   496  	default:
   497  		_ = cmd.UsageFunc()(cmd)
   498  		os.Exit(exitCodeUsage)
   499  	}
   500  	if err != nil {
   501  		die("Could not announce host:", err)
   502  	}
   503  	fmt.Println("Host announcement submitted to network.")
   504  
   505  	// start accepting contracts
   506  	err = httpClient.HostModifySettingPost(client.HostParamAcceptingContracts, true)
   507  	if err != nil {
   508  		die("Could not configure host to accept contracts:", err)
   509  	}
   510  	fmt.Println(`The host has also been configured to accept contracts.
   511  To revert this, run:
   512  	siac host config acceptingcontracts false`)
   513  }
   514  
   515  // hostfolderaddcmd adds a folder to the host.
   516  func hostfolderaddcmd(path, size string) {
   517  	size, err := parseFilesize(size)
   518  	if err != nil {
   519  		die("Could not parse size:", err)
   520  	}
   521  	// round size down to nearest multiple of 256MiB
   522  	var sizeUint64 uint64
   523  	fmt.Sscan(size, &sizeUint64)
   524  	sizeUint64 /= 64 * modules.SectorSize
   525  	sizeUint64 *= 64 * modules.SectorSize
   526  
   527  	err = httpClient.HostStorageFoldersAddPost(abs(path), sizeUint64)
   528  	if err != nil {
   529  		die("Could not add folder:", err)
   530  	}
   531  	fmt.Println("Added folder", path)
   532  }
   533  
   534  // hostfolderremovecmd removes a folder from the host.
   535  func hostfolderremovecmd(path string) {
   536  	// Ask for confirm for dangerous --force flag
   537  	if hostFolderRemoveForce {
   538  		fmt.Println(`Forced removing will completely destroy your renter's data,
   539  	and you will lose your locked collateral.`)
   540  		confirmed := askForConfirmation("Do you want to continue?")
   541  		if !confirmed {
   542  			return
   543  		}
   544  	}
   545  
   546  	err := httpClient.HostStorageFoldersRemovePost(abs(path), hostFolderRemoveForce)
   547  	if err != nil {
   548  		die("Could not remove folder:", err)
   549  	}
   550  	fmt.Println("Removed folder", path)
   551  }
   552  
   553  // hostfolderresizecmd resizes a folder in the host.
   554  func hostfolderresizecmd(path, newsize string) {
   555  	newsize, err := parseFilesize(newsize)
   556  	if err != nil {
   557  		die("Could not parse size:", err)
   558  	}
   559  	// round size down to nearest multiple of 256MiB
   560  	var sizeUint64 uint64
   561  	fmt.Sscan(newsize, &sizeUint64)
   562  	sizeUint64 /= 64 * modules.SectorSize
   563  	sizeUint64 *= 64 * modules.SectorSize
   564  
   565  	err = httpClient.HostStorageFoldersResizePost(abs(path), sizeUint64)
   566  	if err != nil {
   567  		die("Could not resize folder:", err)
   568  	}
   569  	fmt.Printf("Resized folder %v to %v\n", path, newsize)
   570  }
   571  
   572  // hostsectordeletecmd deletes a sector from the host.
   573  func hostsectordeletecmd(root string) {
   574  	var hash crypto.Hash
   575  	err := hash.LoadString(root)
   576  	if err != nil {
   577  		die("Could not parse root:", err)
   578  	}
   579  	err = httpClient.HostStorageSectorsDeletePost(hash)
   580  	if err != nil {
   581  		die("Could not delete sector:", err)
   582  	}
   583  	fmt.Println("Deleted sector", root)
   584  }