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

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/spf13/cobra"
    17  	"github.com/spf13/cobra/doc"
    18  	mnemonics "gitlab.com/NebulousLabs/entropy-mnemonics"
    19  
    20  	"gitlab.com/NebulousLabs/encoding"
    21  	"gitlab.com/SkynetLabs/skyd/siatest"
    22  	"gitlab.com/SkynetLabs/skyd/skymodules"
    23  	"go.sia.tech/siad/crypto"
    24  	"go.sia.tech/siad/modules"
    25  	"go.sia.tech/siad/types"
    26  )
    27  
    28  var (
    29  	utilsCmd = &cobra.Command{
    30  		Use:   "utils",
    31  		Short: "various utilities for working with Sia's types",
    32  		Long: `Various utilities for working with Sia's types.
    33  These commands do not require siad.`,
    34  		// Run field not provided; utils requires a subcommand.
    35  	}
    36  
    37  	bashcomplCmd = &cobra.Command{
    38  		Use:   "bash-completion [path]",
    39  		Short: "Creates bash completion file.",
    40  		Long: `Creates a bash completion file at the specified location.
    41  
    42  Note: Bash completions will only work with the prefix with which the script
    43  is created (e.g. ./siac or siac).
    44  
    45  Once created, the file has to be moved to the bash completion script folder,
    46  usually /etc/bash_completion.d/`,
    47  		Run: wrap(bashcomplcmd),
    48  	}
    49  
    50  	mangenCmd = &cobra.Command{
    51  		Use:   "man-generation [path]",
    52  		Short: "Creates unix style manpages.",
    53  		Long:  "Creates unix style man pages at the specified directory.",
    54  		Run:   wrap(mangencmd),
    55  	}
    56  
    57  	utilsHastingsCmd = &cobra.Command{
    58  		Use:   "hastings [amount]",
    59  		Short: "convert a currency amount to Hastings",
    60  		Long: `Convert a currency amount to Hastings.
    61  See wallet --help for a list of units.`,
    62  		Run: wrap(utilshastingscmd),
    63  	}
    64  
    65  	utilsEncodeRawTxnCmd = &cobra.Command{
    66  		Use:   "encoderawtxn [json txn]",
    67  		Short: "convert a JSON-encoded transaction to base64",
    68  		Long: `Convert a JSON-encoded transaction to base64.
    69  The argument may be either a JSON literal or a file containing JSON.`,
    70  		Run: wrap(utilsencoderawtxncmd),
    71  	}
    72  
    73  	utilsDecodeRawTxnCmd = &cobra.Command{
    74  		Use:   "decoderawtxn [base64 txn]",
    75  		Short: "convert a base64-encoded transaction to JSON",
    76  		Long:  `Convert a base64-encoded transaction to JSON.`,
    77  		Run:   wrap(utilsdecoderawtxncmd),
    78  	}
    79  
    80  	utilsSigHashCmd = &cobra.Command{
    81  		Use:   "sighash [sig index] [txn]",
    82  		Short: "calculate the SigHash of a transaction",
    83  		Long: `Calculate the SigHash of a transaction.
    84  The SigHash is the hash of the fields of the transaction specified
    85  in the CoveredFields of the specified signature.
    86  The transaction may be JSON, base64, or a file containing either.`,
    87  		Run: wrap(utilssighashcmd),
    88  	}
    89  
    90  	utilsCheckSigCmd = &cobra.Command{
    91  		Use:   "checksig [sig] [hash] [pubkey]",
    92  		Short: "verify a signature of the specified hash",
    93  		Long: `Verify that a hash was signed by the specified key.
    94  
    95  The signature should be base64-encoded, and the hash should be hex-encoded.
    96  The pubkey should be either a JSON-encoded SiaPublicKey, or of the form:
    97      algorithm:hexkey
    98  e.g. ed25519:d0e1a2d3b4e5e6f7...
    99  
   100  Use sighash to calculate the hash of a transaction.
   101  `,
   102  		Run: wrap(utilschecksigcmd),
   103  	}
   104  
   105  	utilsVerifySeedCmd = &cobra.Command{
   106  		Use:   "verify-seed",
   107  		Short: "verify seed is formatted correctly",
   108  		Long: `Verify that a seed has correct number of words, no extra whitespace,
   109  and all words appear in the Sia dictionary. The language may be english (default), japanese, or german`,
   110  		Run: wrap(utilsverifyseedcmd),
   111  	}
   112  
   113  	utilsDisplayAPIPasswordCmd = &cobra.Command{
   114  		Use:   "display-api-password",
   115  		Short: "display the API password",
   116  		Long: `Display the API password.  The API password is required for some 3rd 
   117  party integrations such as Duplicati`,
   118  		Run: wrap(utilsdisplayapipasswordcmd),
   119  	}
   120  
   121  	utilsBruteForceSeedCmd = &cobra.Command{
   122  		Use:   "bruteforce-seed",
   123  		Short: "attempt to brute force seed",
   124  		Long: `Attempts to brute force a partial Sia seed.  Accepts a 27 or 28 word
   125  seed and returns a valid 28 or 29 word seed`,
   126  		Run: wrap(utilsbruteforceseedcmd),
   127  	}
   128  
   129  	utilsUploadedsizeCmd = &cobra.Command{
   130  		Use:   "uploadedsize [path]",
   131  		Short: "calculate a folder's size on Sia",
   132  		Long: `Calculates a given folder size on Sia and the lost space caused by 
   133  files are rounded up to the minimum chunks size.`,
   134  		Run: wrap(utilsuploadedsizecmd),
   135  	}
   136  )
   137  
   138  // bashcmlcmd is the handler for the command `skyc utils bash-completion`.
   139  func bashcomplcmd(path string) {
   140  	rootCmd.GenBashCompletionFile(path)
   141  }
   142  
   143  // mangencmd is the handler for the command `skyc utils man-generation`.
   144  // generates siac man pages
   145  func mangencmd(path string) {
   146  	doc.GenManTree(rootCmd, &doc.GenManHeader{
   147  		Section: "1",
   148  		Manual:  "siac Manual",
   149  		Source:  "",
   150  	}, path)
   151  }
   152  
   153  // utilshastingscmd is the handler for the command `skyc utils hastings`.
   154  // converts a Siacoin amount into hastings.
   155  func utilshastingscmd(amount string) {
   156  	hastings, err := types.ParseCurrency(amount)
   157  	if err != nil {
   158  		die(err)
   159  	}
   160  	fmt.Println(hastings)
   161  }
   162  
   163  // utilsdecoderawtxncmd is the handler for command `skyc utils decoderawtxn`.
   164  // converts a base64-encoded transaction to JSON encoding
   165  func utilsdecoderawtxncmd(b64 string) {
   166  	bin, err := base64.StdEncoding.DecodeString(b64)
   167  	if err != nil {
   168  		die("Invalid base64:", err)
   169  	}
   170  	var txn types.Transaction
   171  	if err := encoding.Unmarshal(bin, &txn); err != nil {
   172  		die("Invalid transaction:", err)
   173  	}
   174  	js, _ := json.MarshalIndent(txn, "", "\t")
   175  	fmt.Println(string(js))
   176  }
   177  
   178  // utilsencoderawtxncmd is the handler for command `skyc utils encoderawtxn`.
   179  // converts a JSON encoded transaction to base64-encoding
   180  func utilsencoderawtxncmd(jstxn string) {
   181  	var jsBytes []byte
   182  	if strings.HasPrefix(strings.TrimSpace(jstxn), "{") {
   183  		// assume JSON if arg starts with {
   184  		jsBytes = []byte(jstxn)
   185  	} else {
   186  		// otherwise, assume it's a file containing JSON
   187  		var err error
   188  		jsBytes, err = ioutil.ReadFile(jstxn)
   189  		if err != nil {
   190  			die("Could not read JSON file:", err)
   191  		}
   192  	}
   193  	var txn types.Transaction
   194  	if err := json.Unmarshal(jsBytes, &txn); err != nil {
   195  		die("Invalid transaction:", err)
   196  	}
   197  	fmt.Println(base64.StdEncoding.EncodeToString(encoding.Marshal(txn)))
   198  }
   199  
   200  // utilssighashcmd is the handler for the command `skyc utils sighash`.
   201  // calculates the SigHash of a transaction
   202  func utilssighashcmd(indexStr, txnStr string) {
   203  	index, err := strconv.Atoi(indexStr)
   204  	if err != nil {
   205  		die("Sig index must be an integer")
   206  	}
   207  
   208  	// assume txn is a file
   209  	txnBytes, err := ioutil.ReadFile(txnStr)
   210  	if os.IsNotExist(err) {
   211  		// assume txn is a literal encoding
   212  		txnBytes = []byte(txnStr)
   213  	} else if err != nil {
   214  		die("Could not read JSON file:", err)
   215  	}
   216  	// txnBytes is either JSON or base64
   217  	var txn types.Transaction
   218  	if json.Valid(txnBytes) {
   219  		if err := json.Unmarshal(txnBytes, &txn); err != nil {
   220  			die("Could not decode JSON:", err)
   221  		}
   222  	} else {
   223  		bin, err := base64.StdEncoding.DecodeString(string(txnBytes))
   224  		if err != nil {
   225  			die("Could not decode txn as JSON, base64, or file")
   226  		}
   227  		if err := encoding.Unmarshal(bin, &txn); err != nil {
   228  			die("Could not decode binary transaction:", err)
   229  		}
   230  	}
   231  
   232  	fmt.Println(txn.SigHash(index, 180e3))
   233  }
   234  
   235  // utilschecksigcmd is the handler for the command `skyc utils checksig`.
   236  // verifies the signature of a hash
   237  func utilschecksigcmd(base64Sig, hexHash, pkStr string) {
   238  	var sig crypto.Signature
   239  	sigBytes, err := base64.StdEncoding.DecodeString(base64Sig)
   240  	if err != nil || copy(sig[:], sigBytes) != len(sig) {
   241  		die("Couldn't parse signature")
   242  	}
   243  	var hash crypto.Hash
   244  	if err := hash.LoadString(hexHash); err != nil {
   245  		die("Couldn't parse hash")
   246  	}
   247  	var spk types.SiaPublicKey
   248  	if spk.LoadString(pkStr); len(spk.Key) == 0 {
   249  		if err := json.Unmarshal([]byte(pkStr), &spk); err != nil {
   250  			die("Couldn't parse pubkey")
   251  		}
   252  	}
   253  	if spk.Algorithm != types.SignatureEd25519 {
   254  		die("Only ed25519 signatures are supported")
   255  	}
   256  	var pk crypto.PublicKey
   257  	copy(pk[:], spk.Key)
   258  
   259  	if crypto.VerifyHash(hash, pk, sig) == nil {
   260  		fmt.Println("Verified OK")
   261  	} else {
   262  		log.Fatalln("Bad signature")
   263  	}
   264  }
   265  
   266  // utilsverifyseedcmd is the handler for the command `skyc utils verify-seed`.
   267  // verifies a seed matches the required formatting.  This can be used to help
   268  // troubleshot seeds that are not being accepted by siad.
   269  func utilsverifyseedcmd() {
   270  	seed, err := passwordPrompt("Please enter your seed: ")
   271  	if err != nil {
   272  		die("Could not read seed")
   273  	}
   274  
   275  	_, err = modules.StringToSeed(seed, mnemonics.DictionaryID(strings.ToLower(dictionaryLanguage)))
   276  	if err != nil {
   277  		die(err)
   278  	}
   279  	fmt.Println("No issues detected with your seed")
   280  }
   281  
   282  // utilsdisplayapipasswordcmd is the handler for the command `skyc utils
   283  // display-api-password`.
   284  // displays the API Password to the user.
   285  func utilsdisplayapipasswordcmd() {
   286  	fmt.Println(httpClient.Password)
   287  }
   288  
   289  // utilsbruteforceseedcmd is the handler for the command `skyc utils
   290  // bruteforce-seed`
   291  // attempts to find the one word missing from a seed.
   292  func utilsbruteforceseedcmd() {
   293  	fmt.Println("Enter partial seed: ")
   294  	s := bufio.NewScanner(os.Stdin)
   295  	s.Scan()
   296  	if s.Err() != nil {
   297  		log.Fatal("Couldn't read seed:", s.Err())
   298  	}
   299  	knownWords := strings.Fields(s.Text())
   300  	if len(knownWords) != 27 && len(knownWords) != 28 {
   301  		log.Fatalln("Expected 27 or 28 words in partial seed, got", len(knownWords))
   302  	}
   303  	allWords := make([]string, len(knownWords)+1)
   304  	var did mnemonics.DictionaryID = "english"
   305  	var checked int
   306  	total := len(allWords) * len(mnemonics.EnglishDictionary)
   307  	for i := range allWords {
   308  		copy(allWords[:i], knownWords[:i])
   309  		copy(allWords[i+1:], knownWords[i:])
   310  		for _, word := range mnemonics.EnglishDictionary {
   311  			allWords[i] = word
   312  			s := strings.Join(allWords, " ")
   313  			checksumSeedBytes, _ := mnemonics.FromString(s, did)
   314  			var seed modules.Seed
   315  			copy(seed[:], checksumSeedBytes)
   316  			fullChecksum := crypto.HashObject(seed)
   317  			if len(checksumSeedBytes) == crypto.EntropySize+modules.SeedChecksumSize && bytes.Equal(fullChecksum[:modules.SeedChecksumSize], checksumSeedBytes[crypto.EntropySize:]) {
   318  				if _, err := modules.StringToSeed(s, mnemonics.English); err == nil {
   319  					fmt.Printf("\nFound valid seed! The missing word was %q\n", word)
   320  					fmt.Println(s)
   321  					return
   322  				}
   323  			}
   324  			checked++
   325  			fmt.Printf("\rChecked %v/%v...", checked, total)
   326  		}
   327  	}
   328  	fmt.Printf("\nNo valid seed found :(\n")
   329  }
   330  
   331  // utilsuploadedsizecmd is the handler for the command `utils uploadedsize [path] [flags]`
   332  // It estimates the 'on Sia' size of the given directory
   333  func utilsuploadedsizecmd(path string) {
   334  	var fileSizes []uint64
   335  	if fileExists(path) {
   336  		fi, err := os.Stat(path)
   337  		if err != nil {
   338  			fmt.Println("Error: could not determine the file size")
   339  			return
   340  		}
   341  		fileSizes = append(fileSizes, uint64(fi.Size()))
   342  	} else {
   343  		err := filepath.Walk( // export all file sizes to fileSizes slice (recursive)
   344  			path,
   345  			func(path string, info os.FileInfo, err error) error {
   346  				if err != nil {
   347  					return err
   348  				}
   349  				if !info.IsDir() {
   350  					fileSizes = append(fileSizes, uint64(info.Size()))
   351  				}
   352  				return nil
   353  			})
   354  		if err != nil {
   355  			fmt.Println("Error walking directory:", err)
   356  			return
   357  		}
   358  	}
   359  
   360  	var diskSize, siaSize, lostPercent uint64
   361  	minFileSize := siatest.ChunkSize(uint64(skymodules.RenterDefaultDataPieces), crypto.TypeDefaultRenter)
   362  
   363  	for _, size := range fileSizes { // Calc variables here
   364  		diskSize += size
   365  
   366  		// Round file size to 40MiB chunks
   367  		numChunks := uint64(size / minFileSize)
   368  		if size%minFileSize != 0 {
   369  			numChunks++
   370  		}
   371  		siaSize += numChunks * minFileSize
   372  	}
   373  
   374  	if diskSize != 0 {
   375  		lostPercent = uint64(float64(siaSize)/float64(diskSize)*100) - 100
   376  	}
   377  	fmt.Printf(`Size on
   378      Disk: %v
   379      Sia:  %v
   380  
   381  Lost space: %v
   382      +%v%% empty space used for scaling every file up to %v
   383  `,
   384  		modules.FilesizeUnits(diskSize),
   385  		modules.FilesizeUnits(siaSize),
   386  		modules.FilesizeUnits(siaSize-diskSize),
   387  		lostPercent,
   388  		modules.FilesizeUnits(minFileSize))
   389  
   390  	if verbose { // print only if -v or --verbose used
   391  		fmt.Printf(`
   392  Files: %v
   393      Average: %v
   394      Median: %v
   395  `,
   396  			len(fileSizes),
   397  			modules.FilesizeUnits(calculateAverageUint64(fileSizes)),
   398  			modules.FilesizeUnits(calculateMedianUint64(fileSizes)))
   399  	}
   400  }