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

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"os"
     7  	"text/tabwriter"
     8  
     9  	"github.com/spf13/cobra"
    10  
    11  	"gitlab.com/NebulousLabs/errors"
    12  	"gitlab.com/SkynetLabs/skyd/node/api"
    13  	"gitlab.com/SkynetLabs/skyd/skymodules"
    14  	"go.sia.tech/siad/modules"
    15  	"go.sia.tech/siad/types"
    16  )
    17  
    18  const scanHistoryLen = 30
    19  
    20  var (
    21  	hostdbNumHosts int
    22  )
    23  
    24  var (
    25  	hostdbCmd = &cobra.Command{
    26  		Use:   "hostdb",
    27  		Short: "Interact with the renter's host database.",
    28  		Long:  "View the list of active hosts, the list of all hosts, or query specific hosts.\nIf the '-v' flag is set, a list of recent scans will be provided, with the most\nrecent scan on the right. a '0' indicates that the host was offline, and a '1'\nindicates that the host was online.",
    29  		Run:   wrap(hostdbcmd),
    30  	}
    31  
    32  	hostdbBlockDomainCmd = &cobra.Command{
    33  		Use:   "block [domain] [domain]...",
    34  		Short: "Block a domain in the hostdb",
    35  		Long:  "Block a domain or list of domains in the hostdb",
    36  		Run:   hostdbblockdomaincmd,
    37  	}
    38  
    39  	hostdbBlockedDomainsCmd = &cobra.Command{
    40  		Use:   "blocked",
    41  		Short: "View blocked domains in the hostdb",
    42  		Long:  "View blocked domains in the hostdb",
    43  		Run:   wrap(hostdbblockeddomainscmd),
    44  	}
    45  
    46  	hostdbFiltermodeCmd = &cobra.Command{
    47  		Use:   "filtermode",
    48  		Short: "View hostDB filtermode.",
    49  		Long:  "View the hostDB filtermode and the filtered hosts",
    50  		Run:   wrap(hostdbfiltermodecmd),
    51  	}
    52  
    53  	hostdbSetFiltermodeCmd = &cobra.Command{
    54  		Use:   "setfiltermode [filtermode] [host] [host] [host]...",
    55  		Short: "Set the filtermode.",
    56  		Long: `Set the hostdb filtermode and specify hosts.
    57          [filtermode] can be whitelist, blacklist, or disable.
    58          [host] is the host public key.`,
    59  		Run: hostdbsetfiltermodecmd,
    60  	}
    61  
    62  	hostdbUnblockDomainCmd = &cobra.Command{
    63  		Use:   "unblock [domain] [domain]...",
    64  		Short: "Unblock a domain in the hostdb",
    65  		Long:  "Unblock a domain or list of domains in the hostdb",
    66  		Run:   hostdbunblockdomaincmd,
    67  	}
    68  
    69  	hostdbViewCmd = &cobra.Command{
    70  		Use:   "view [pubkey]",
    71  		Short: "View the full information for a host.",
    72  		Long:  "View detailed information about a host, including things like a score breakdown.",
    73  		Run:   wrap(hostdbviewcmd),
    74  	}
    75  )
    76  
    77  // printScoreBreakdown prints the score breakdown of a host, provided the info.
    78  func printScoreBreakdown(info *api.HostdbHostsGET) {
    79  	fmt.Println("\n  Score Breakdown:")
    80  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
    81  	fmt.Fprintf(w, "\t\tAge:\t %.3f\n", info.ScoreBreakdown.AgeAdjustment)
    82  	fmt.Fprintf(w, "\t\tBase Price:\t %.3f\n", info.ScoreBreakdown.BasePriceAdjustment)
    83  	fmt.Fprintf(w, "\t\tBurn:\t %.3f\n", info.ScoreBreakdown.BurnAdjustment)
    84  	fmt.Fprintf(w, "\t\tCollateral:\t %.3f\n", info.ScoreBreakdown.CollateralAdjustment/1e96)
    85  	fmt.Fprintf(w, "\t\tDuration:\t %.3f\n", info.ScoreBreakdown.DurationAdjustment)
    86  	fmt.Fprintf(w, "\t\tInteraction:\t %.3f\n", info.ScoreBreakdown.InteractionAdjustment)
    87  	fmt.Fprintf(w, "\t\tPrice:\t %.3f\n", info.ScoreBreakdown.PriceAdjustment*1e24)
    88  	fmt.Fprintf(w, "\t\tStorage:\t %.3f\n", info.ScoreBreakdown.StorageRemainingAdjustment)
    89  	fmt.Fprintf(w, "\t\tUptime:\t %.3f\n", info.ScoreBreakdown.UptimeAdjustment)
    90  	fmt.Fprintf(w, "\t\tVersion:\t %.3f\n", info.ScoreBreakdown.VersionAdjustment)
    91  	fmt.Fprintf(w, "\t\tConversion Rate:\t %.3f\n", info.ScoreBreakdown.ConversionRate)
    92  	if err := w.Flush(); err != nil {
    93  		die("failed to flush writer")
    94  	}
    95  }
    96  
    97  // hostdbcmd is the handler for the command `skyc hostdb`.
    98  // Lists hosts known to the hostdb
    99  func hostdbcmd() {
   100  	if !verbose {
   101  		info, err := httpClient.HostDbActiveGet()
   102  		if errors.Contains(err, api.ErrAPICallNotRecognized) {
   103  			// Assume module is not loaded if status command is not recognized.
   104  			fmt.Printf("HostDB:\n  Status: %s\n\n", moduleNotReadyStatus)
   105  			return
   106  		} else if err != nil {
   107  			die("Could not fetch host list:", err)
   108  		}
   109  
   110  		if len(info.Hosts) == 0 {
   111  			fmt.Println("No known active hosts")
   112  			return
   113  		}
   114  
   115  		// Strip down to the number of requested hosts.
   116  		if hostdbNumHosts != 0 && hostdbNumHosts < len(info.Hosts) {
   117  			info.Hosts = info.Hosts[len(info.Hosts)-hostdbNumHosts:]
   118  		}
   119  
   120  		fmt.Println(len(info.Hosts), "Active Hosts:")
   121  		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   122  		fmt.Fprintln(w, "\t\tAddress\tVersion\tPrice (per TB per Mo)")
   123  		for i, host := range info.Hosts {
   124  			price := host.StoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)
   125  			fmt.Fprintf(w, "\t%v:\t%v\t%v\t%v\n", len(info.Hosts)-i, host.NetAddress, host.Version, currencyUnits(price))
   126  		}
   127  		if err := w.Flush(); err != nil {
   128  			die("failed to flush writer")
   129  		}
   130  	} else {
   131  		info, err := httpClient.HostDbAllGet()
   132  		if err != nil {
   133  			die("Could not fetch host list:", err)
   134  		}
   135  		if len(info.Hosts) == 0 {
   136  			fmt.Println("No known hosts")
   137  			return
   138  		}
   139  
   140  		// Iterate through the hosts and divide by category.
   141  		var activeHosts, inactiveHosts, offlineHosts []api.ExtendedHostDBEntry
   142  		for _, host := range info.Hosts {
   143  			if host.AcceptingContracts && len(host.ScanHistory) > 0 && host.ScanHistory[len(host.ScanHistory)-1].Success {
   144  				activeHosts = append(activeHosts, host)
   145  				continue
   146  			}
   147  			if len(host.ScanHistory) > 0 && host.ScanHistory[len(host.ScanHistory)-1].Success {
   148  				inactiveHosts = append(inactiveHosts, host)
   149  				continue
   150  			}
   151  			offlineHosts = append(offlineHosts, host)
   152  		}
   153  
   154  		if hostdbNumHosts > 0 && len(offlineHosts) > hostdbNumHosts {
   155  			offlineHosts = offlineHosts[len(offlineHosts)-hostdbNumHosts:]
   156  		}
   157  		if hostdbNumHosts > 0 && len(inactiveHosts) > hostdbNumHosts {
   158  			inactiveHosts = inactiveHosts[len(inactiveHosts)-hostdbNumHosts:]
   159  		}
   160  		if hostdbNumHosts > 0 && len(activeHosts) > hostdbNumHosts {
   161  			activeHosts = activeHosts[len(activeHosts)-hostdbNumHosts:]
   162  		}
   163  
   164  		fmt.Println()
   165  		fmt.Println(len(offlineHosts), "Offline Hosts:")
   166  		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   167  		fmt.Fprintln(w, "\t\tPubkey\tAddress\tVersion\tRemaining Storage\tPrice (/ TB / Month)\tDownload Price (/ TB)\tUptime\tRecent Scans")
   168  		for i, host := range offlineHosts {
   169  			// Compute the total measured uptime and total measured downtime for this
   170  			// host.
   171  			uptimeRatio := float64(0)
   172  			if len(host.ScanHistory) > 1 {
   173  				downtime := host.HistoricDowntime
   174  				uptime := host.HistoricUptime
   175  				recentTime := host.ScanHistory[0].Timestamp
   176  				recentSuccess := host.ScanHistory[0].Success
   177  				for _, scan := range host.ScanHistory[1:] {
   178  					if recentSuccess {
   179  						uptime += scan.Timestamp.Sub(recentTime)
   180  					} else {
   181  						downtime += scan.Timestamp.Sub(recentTime)
   182  					}
   183  					recentTime = scan.Timestamp
   184  					recentSuccess = scan.Success
   185  				}
   186  				uptimeRatio = float64(uptime) / float64(uptime+downtime)
   187  			}
   188  
   189  			// Get the scan history string.
   190  			scanHistStr := ""
   191  			displayScans := host.ScanHistory
   192  			if len(host.ScanHistory) > scanHistoryLen {
   193  				displayScans = host.ScanHistory[len(host.ScanHistory)-scanHistoryLen:]
   194  			}
   195  			for _, scan := range displayScans {
   196  				if scan.Success {
   197  					scanHistStr += "1"
   198  				} else {
   199  					scanHistStr += "0"
   200  				}
   201  			}
   202  
   203  			// Get a string representation of the historic outcomes of the most
   204  			// recent scans.
   205  			price := host.StoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)
   206  			downloadBWPrice := host.StoragePrice.Mul(modules.BytesPerTerabyte)
   207  			fmt.Fprintf(w, "\t%v:\t%v\t%v\t%v\t%v\t%v\t%v\t%.3f\t%s\n", len(offlineHosts)-i, host.PublicKeyString,
   208  				host.NetAddress, host.Version, modules.FilesizeUnits(host.RemainingStorage), currencyUnits(price), currencyUnits(downloadBWPrice), uptimeRatio, scanHistStr)
   209  		}
   210  		if err := w.Flush(); err != nil {
   211  			die("failed to flush writer")
   212  		}
   213  
   214  		fmt.Println()
   215  		fmt.Println(len(inactiveHosts), "Inactive Hosts:")
   216  		w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   217  		fmt.Fprintln(w, "\t\tPubkey\tAddress\tVersion\tRemaining Storage\tPrice (/ TB / Month)\tCollateral (/ TB / Month)\tDownload Price (/ TB)\tUptime\tRecent Scans")
   218  		for i, host := range inactiveHosts {
   219  			// Compute the total measured uptime and total measured downtime for this
   220  			// host.
   221  			uptimeRatio := float64(0)
   222  			if len(host.ScanHistory) > 1 {
   223  				downtime := host.HistoricDowntime
   224  				uptime := host.HistoricUptime
   225  				recentTime := host.ScanHistory[0].Timestamp
   226  				recentSuccess := host.ScanHistory[0].Success
   227  				for _, scan := range host.ScanHistory[1:] {
   228  					if recentSuccess {
   229  						uptime += scan.Timestamp.Sub(recentTime)
   230  					} else {
   231  						downtime += scan.Timestamp.Sub(recentTime)
   232  					}
   233  					recentTime = scan.Timestamp
   234  					recentSuccess = scan.Success
   235  				}
   236  				uptimeRatio = float64(uptime) / float64(uptime+downtime)
   237  			}
   238  
   239  			// Get a string representation of the historic outcomes of the most
   240  			// recent scans.
   241  			scanHistStr := ""
   242  			displayScans := host.ScanHistory
   243  			if len(host.ScanHistory) > scanHistoryLen {
   244  				displayScans = host.ScanHistory[len(host.ScanHistory)-scanHistoryLen:]
   245  			}
   246  			for _, scan := range displayScans {
   247  				if scan.Success {
   248  					scanHistStr += "1"
   249  				} else {
   250  					scanHistStr += "0"
   251  				}
   252  			}
   253  
   254  			price := host.StoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)
   255  			collateral := host.Collateral.Mul(modules.BlockBytesPerMonthTerabyte)
   256  			downloadBWPrice := host.DownloadBandwidthPrice.Mul(modules.BytesPerTerabyte)
   257  			fmt.Fprintf(w, "\t%v:\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%.3f\t%s\n", len(inactiveHosts)-i, host.PublicKeyString, host.NetAddress, host.Version, modules.FilesizeUnits(host.RemainingStorage), currencyUnits(price), currencyUnits(collateral), currencyUnits(downloadBWPrice), uptimeRatio, scanHistStr)
   258  		}
   259  		fmt.Fprintln(w, "\t\tPubkey\tAddress\tVersion\tRemaining Storage\tPrice (/ TB / Month)\tCollateral (/ TB / Month)\tDownload Price (/ TB)\tUptime\tRecent Scans")
   260  		if err := w.Flush(); err != nil {
   261  			die("failed to flush writer")
   262  		}
   263  
   264  		// Grab the host at the 3/5th point and use it as the reference. (it's
   265  		// like using the median, except at the 3/5th point instead of the 1/2
   266  		// point.)
   267  		referenceScore := big.NewRat(1, 1)
   268  		if len(activeHosts) > 0 {
   269  			referenceIndex := len(activeHosts) * 3 / 5
   270  			hostInfo, err := httpClient.HostDbHostsGet(activeHosts[referenceIndex].PublicKey)
   271  			if err != nil {
   272  				die("Could not fetch provided host:", err)
   273  			}
   274  			if !hostInfo.ScoreBreakdown.Score.IsZero() {
   275  				referenceScore = new(big.Rat).Inv(new(big.Rat).SetInt(hostInfo.ScoreBreakdown.Score.Big()))
   276  			}
   277  		}
   278  
   279  		fmt.Println()
   280  		fmt.Println(len(activeHosts), "Active Hosts:")
   281  		w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   282  		fmt.Fprintln(w, "\t\tPubkey\tAddress\tVersion\tScore\tRemaining Storage\tContract Fee\tPrice (/ TB / Month)\tCollateral (/ TB / Month)\tDownload Price (/TB)\tUptime\tRecent Scans")
   283  		for i, host := range activeHosts {
   284  			// Compute the total measured uptime and total measured downtime for this
   285  			// host.
   286  			uptimeRatio := float64(0)
   287  			if len(host.ScanHistory) > 1 {
   288  				downtime := host.HistoricDowntime
   289  				uptime := host.HistoricUptime
   290  				recentTime := host.ScanHistory[0].Timestamp
   291  				recentSuccess := host.ScanHistory[0].Success
   292  				for _, scan := range host.ScanHistory[1:] {
   293  					if recentSuccess {
   294  						uptime += scan.Timestamp.Sub(recentTime)
   295  					} else {
   296  						downtime += scan.Timestamp.Sub(recentTime)
   297  					}
   298  					recentTime = scan.Timestamp
   299  					recentSuccess = scan.Success
   300  				}
   301  				uptimeRatio = float64(uptime) / float64(uptime+downtime)
   302  			}
   303  
   304  			// Get a string representation of the historic outcomes of the most
   305  			// recent scans.
   306  			scanHistStr := ""
   307  			displayScans := host.ScanHistory
   308  			if len(host.ScanHistory) > scanHistoryLen {
   309  				displayScans = host.ScanHistory[len(host.ScanHistory)-scanHistoryLen:]
   310  			}
   311  			for _, scan := range displayScans {
   312  				if scan.Success {
   313  					scanHistStr += "1"
   314  				} else {
   315  					scanHistStr += "0"
   316  				}
   317  			}
   318  
   319  			// Grab the score information for the active hosts.
   320  			hostInfo, err := httpClient.HostDbHostsGet(host.PublicKey)
   321  			if err != nil {
   322  				die("Could not fetch provided host:", err)
   323  			}
   324  			score, _ := new(big.Rat).Mul(referenceScore, new(big.Rat).SetInt(hostInfo.ScoreBreakdown.Score.Big())).Float64()
   325  
   326  			price := host.StoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)
   327  			collateral := host.Collateral.Mul(modules.BlockBytesPerMonthTerabyte)
   328  			downloadBWPrice := host.DownloadBandwidthPrice.Mul(modules.BytesPerTerabyte)
   329  			fmt.Fprintf(w, "\t%v:\t%v\t%v\t%v\t%12.6g\t%v\t%v\t%v\t%v\t%v\t%.3f\t%s\n", len(activeHosts)-i, host.PublicKeyString, host.NetAddress, host.Version, score, modules.FilesizeUnits(host.RemainingStorage), currencyUnits(host.ContractPrice), currencyUnits(price), currencyUnits(collateral), currencyUnits(downloadBWPrice), uptimeRatio, scanHistStr)
   330  		}
   331  		fmt.Fprintln(w, "\t\tPubkey\tAddress\tVersion\tScore\tRemaining Storage\tContract Fee\tPrice (/ TB / Month)\tCollateral (/ TB / Month)\tDownload Price (/TB)\tUptime\tRecent Scans")
   332  		if err := w.Flush(); err != nil {
   333  			die("failed to flush writer")
   334  		}
   335  	}
   336  }
   337  
   338  // hostdbblockdomaincmd is the handler for the command `skyc hostdb
   339  // block`.
   340  func hostdbblockdomaincmd(cmd *cobra.Command, domains []string) {
   341  	if len(domains) == 0 {
   342  		_ = cmd.UsageFunc()(cmd)
   343  		os.Exit(exitCodeUsage)
   344  	}
   345  	err := httpClient.HostDbBlockDomainsPost(domains)
   346  	if err != nil {
   347  		die("Could not block domain in hostdb:", err)
   348  	}
   349  	fmt.Println("Successfully Blocked Domain")
   350  }
   351  
   352  // hostdbblockeddomainscmd is the handler for the command `skyc hostdb
   353  // blockeddomains`.
   354  func hostdbblockeddomainscmd() {
   355  	hdbdg, err := httpClient.HostDbBlockedDomainsGet()
   356  	if err != nil {
   357  		die(err)
   358  	}
   359  	fmt.Println()
   360  	fmt.Println("  Blocked Domains:")
   361  	for _, domain := range hdbdg.Domains {
   362  		fmt.Println("    ", domain)
   363  	}
   364  	fmt.Println()
   365  }
   366  
   367  // hostdbfiltermodecmd is the handler for the command `skyc hostdb
   368  // filtermode`.
   369  func hostdbfiltermodecmd() {
   370  	hdfmg, err := httpClient.HostDbFilterModeGet()
   371  	if err != nil {
   372  		die(err)
   373  	}
   374  	fmt.Println()
   375  	fmt.Println("  HostDB Filter Mode:", hdfmg.FilterMode)
   376  	fmt.Println("  Hosts:")
   377  	for _, host := range hdfmg.Hosts {
   378  		fmt.Println("    ", host)
   379  	}
   380  	fmt.Println()
   381  }
   382  
   383  // hostdbsetfiltermodecmd is the handler for the command `skyc hostdb
   384  // setfiltermode`. sets the hostdb filtermode (whitelist, blacklist, disable)
   385  func hostdbsetfiltermodecmd(cmd *cobra.Command, args []string) {
   386  	var fm skymodules.FilterMode
   387  	var filterModeStr string
   388  	var host types.SiaPublicKey
   389  	var hosts []types.SiaPublicKey
   390  	switch len(args) {
   391  	case 0:
   392  		_ = cmd.UsageFunc()(cmd)
   393  		os.Exit(exitCodeUsage)
   394  	case 1:
   395  		filterModeStr = args[0]
   396  		if filterModeStr != "disable" {
   397  			die("if only submitting filtermode it should be `disable`")
   398  		}
   399  	default:
   400  		filterModeStr = args[0]
   401  		for i := 1; i < len(args); i++ {
   402  			host.LoadString(args[i])
   403  			hosts = append(hosts, host)
   404  		}
   405  	}
   406  	err := fm.FromString(filterModeStr)
   407  	if err != nil {
   408  		fmt.Println("Could not parse filtermode: ", err)
   409  		die()
   410  	}
   411  
   412  	err = httpClient.HostDbFilterModePost(fm, hosts)
   413  	if err != nil {
   414  		fmt.Println("Could not set hostdb filtermode: ", err)
   415  		die()
   416  	}
   417  	fmt.Println("Successfully set the filter mode")
   418  }
   419  
   420  // hostdbunblockdomaincmd is the handler for the command `skyc hostdb
   421  // unblock`.
   422  func hostdbunblockdomaincmd(cmd *cobra.Command, domains []string) {
   423  	if len(domains) == 0 {
   424  		_ = cmd.UsageFunc()(cmd)
   425  		os.Exit(exitCodeUsage)
   426  	}
   427  	err := httpClient.HostDbUnblockDomainsPost(domains)
   428  	if err != nil {
   429  		die("Could not unblock domain in hostdb:", err)
   430  	}
   431  	fmt.Println("Successfully Unblocked Domain")
   432  }
   433  
   434  // hostdbviewcmd is the handler for the command `skyc hostdb view`.
   435  // shows detailed information about a host in the hostdb.
   436  func hostdbviewcmd(pubkey string) {
   437  	var publicKey types.SiaPublicKey
   438  	publicKey.LoadString(pubkey)
   439  	info, err := httpClient.HostDbHostsGet(publicKey)
   440  	if err != nil {
   441  		die("Could not fetch provided host:", err)
   442  	}
   443  
   444  	fmt.Println("Host information:")
   445  
   446  	fmt.Println("  Public Key:               ", info.Entry.PublicKeyString)
   447  	fmt.Println("  Version:                  ", info.Entry.Version)
   448  	fmt.Println("  Block First Seen:         ", info.Entry.FirstSeen)
   449  	fmt.Println("  Absolute Score:           ", info.ScoreBreakdown.Score)
   450  	fmt.Println("  Filtered:                 ", info.Entry.Filtered)
   451  	fmt.Println("  NetAddress:               ", info.Entry.NetAddress)
   452  	fmt.Println("  Last IP Net Change:       ", info.Entry.LastIPNetChange)
   453  	fmt.Println("  Number of IP Net Changes: ", len(info.Entry.IPNets))
   454  
   455  	fmt.Println("\n  Host Settings:")
   456  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   457  
   458  	fmt.Fprintln(w, "\t\tAccepting Contracts:\t", info.Entry.AcceptingContracts)
   459  	fmt.Fprintln(w, "\t\tMax Duration:\t", info.Entry.MaxDuration)
   460  	fmt.Fprintln(w, "\t\tWindow Size:\t", info.Entry.WindowSize)
   461  	fmt.Fprintln(w, "\t\tTotal Storage:\t", modules.FilesizeUnits(info.Entry.TotalStorage))
   462  	fmt.Fprintln(w, "\t\tRemaining Storage:\t", modules.FilesizeUnits(info.Entry.RemainingStorage))
   463  	fmt.Fprintln(w, "\t\tMax Download Batch Size:\t", modules.FilesizeUnits(info.Entry.MaxDownloadBatchSize))
   464  	fmt.Fprintln(w, "\t\tMax Revision Batch Size:\t", modules.FilesizeUnits(info.Entry.MaxReviseBatchSize))
   465  	fmt.Fprintln(w, "\t\tSector Size:\t", modules.FilesizeUnits(info.Entry.SectorSize))
   466  	fmt.Fprintln(w, "\n\t\tOffered Collateral (TB / Mo):\t", currencyUnits(info.Entry.Collateral.Mul(modules.BlockBytesPerMonthTerabyte)))
   467  	fmt.Fprintln(w, "\t\tMax Collateral:\t", currencyUnits(info.Entry.MaxCollateral))
   468  	fmt.Fprintln(w, "\t\tContract Price:\t", currencyUnits(info.Entry.ContractPrice))
   469  	fmt.Fprintln(w, "\t\tBase RPC Price:\t", currencyUnits(info.Entry.BaseRPCPrice))
   470  	fmt.Fprintln(w, "\t\tSector Access Price:\t", currencyUnits(info.Entry.SectorAccessPrice))
   471  	fmt.Fprintln(w, "\t\tStorage Price (TB / Mo):\t", currencyUnits(info.Entry.StoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)))
   472  	fmt.Fprintln(w, "\t\tDownload Price (1 TB):\t", currencyUnits(info.Entry.DownloadBandwidthPrice.Mul(modules.BytesPerTerabyte)))
   473  	fmt.Fprintln(w, "\t\tUpload Price (1 TB):\t", currencyUnits(info.Entry.UploadBandwidthPrice.Mul(modules.BytesPerTerabyte)))
   474  	fmt.Fprintln(w, "\t\tUnlock Hash:\t", info.Entry.UnlockHash)
   475  	fmt.Fprintln(w, "\n\t\tVersion:\t", info.Entry.Version)
   476  	fmt.Fprintln(w, "\t\tRevision Number:\t", info.Entry.RevisionNumber)
   477  	if err := w.Flush(); err != nil {
   478  		die("failed to flush writer")
   479  	}
   480  
   481  	printScoreBreakdown(&info)
   482  
   483  	// Compute the total measured uptime and total measured downtime for this
   484  	// host.
   485  	uptimeRatio := float64(0)
   486  	if len(info.Entry.ScanHistory) > 1 {
   487  		downtime := info.Entry.HistoricDowntime
   488  		uptime := info.Entry.HistoricUptime
   489  		recentTime := info.Entry.ScanHistory[0].Timestamp
   490  		recentSuccess := info.Entry.ScanHistory[0].Success
   491  		for _, scan := range info.Entry.ScanHistory[1:] {
   492  			if recentSuccess {
   493  				uptime += scan.Timestamp.Sub(recentTime)
   494  			} else {
   495  				downtime += scan.Timestamp.Sub(recentTime)
   496  			}
   497  			recentTime = scan.Timestamp
   498  			recentSuccess = scan.Success
   499  		}
   500  		uptimeRatio = float64(uptime) / float64(uptime+downtime)
   501  	}
   502  
   503  	// Compute the uptime ratio, but shift by 0.02 to acknowledge fully that
   504  	// 98% uptime and 100% uptime is valued the same.
   505  	fmt.Println("\n  Scan History Length:              ", len(info.Entry.ScanHistory))
   506  	fmt.Println("  Historic Downtime:                ", info.Entry.HistoricDowntime)
   507  	fmt.Println("  Historic Uptime:                  ", info.Entry.HistoricUptime)
   508  	fmt.Printf("  Historic Failed Interactions:      %.3f\n", info.Entry.HistoricFailedInteractions)
   509  	fmt.Printf("  Historic Successful Interactions:  %.3f\n", info.Entry.HistoricSuccessfulInteractions)
   510  	fmt.Println("  Recent Failed Interactions:       ", info.Entry.RecentFailedInteractions)
   511  	fmt.Println("  Recent Successful Interactions:   ", info.Entry.RecentSuccessfulInteractions)
   512  	fmt.Printf("  Overall Uptime:                    %.3f\n", uptimeRatio)
   513  
   514  	fmt.Println()
   515  }