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 }