1// SPDX-License-Identifier: ISC 2// Copyright (c) 2014-2020 Bitmark Inc. 3// Use of this source code is governed by an ISC 4// license that can be found in the LICENSE file. 5 6package main 7 8import ( 9 "bytes" 10 "crypto/tls" 11 "encoding/json" 12 "fmt" 13 "io/ioutil" 14 "net" 15 "os" 16 "path/filepath" 17 "strconv" 18 "strings" 19 20 "github.com/bitmark-inc/bitmarkd/account" 21 "github.com/bitmark-inc/bitmarkd/block" 22 "github.com/bitmark-inc/bitmarkd/blockheader" 23 "github.com/bitmark-inc/bitmarkd/fault" 24 "github.com/bitmark-inc/bitmarkd/zmqutil" 25 "github.com/bitmark-inc/exitwithstatus" 26 "github.com/bitmark-inc/logger" 27) 28 29const ( 30 peerPublicKeyFilename = "peer.public" 31 peerPrivateKeyFilename = "peer.private" 32 33 rpcCertificateKeyFilename = "rpc.crt" 34 rpcPrivateKeyFilename = "rpc.key" 35 36 proofPublicKeyFilename = "proof.public" 37 proofPrivateKeyFilename = "proof.private" 38 proofLiveSigningKeyFilename = "proof.live" 39 proofTestSigningKeyFilename = "proof.test" 40) 41 42// setup command handler 43// 44// commands that run to create key and certificate files these 45// commands cannot access any internal database or states or the 46// configuration file 47func processSetupCommand(program string, arguments []string) bool { 48 49 command := "help" 50 if len(arguments) > 0 { 51 command = arguments[0] 52 arguments = arguments[1:] 53 } 54 55 switch command { 56 case "gen-peer-identity", "peer": 57 publicKeyFilename := getFilenameWithDirectory(arguments, peerPublicKeyFilename) 58 privateKeyFilename := getFilenameWithDirectory(arguments, peerPrivateKeyFilename) 59 err := zmqutil.MakeKeyPair(publicKeyFilename, privateKeyFilename) 60 if nil != err { 61 fmt.Printf("generate private key: %q and public key: %q error: %s\n", privateKeyFilename, publicKeyFilename, err) 62 exitwithstatus.Exit(1) 63 } 64 fmt.Printf("generated private key: %q and public key: %q\n", privateKeyFilename, publicKeyFilename) 65 66 case "gen-rpc-cert", "rpc": 67 certificateFilename := getFilenameWithDirectory(arguments, rpcCertificateKeyFilename) 68 privateKeyFilename := getFilenameWithDirectory(arguments, rpcPrivateKeyFilename) 69 70 addresses := []string{} 71 if len(arguments) >= 2 { 72 for _, a := range arguments[1:] { 73 if "" != a { 74 addresses = append(addresses, a) 75 } 76 } 77 } 78 79 err := makeSelfSignedCertificate("rpc", certificateFilename, privateKeyFilename, 0 != len(addresses), addresses) 80 if nil != err { 81 fmt.Printf("generate RPC key: %q and certificate: %q error: %s\n", privateKeyFilename, certificateFilename, err) 82 exitwithstatus.Exit(1) 83 } 84 fmt.Printf("generated RPC key: %q and certificate: %q\n", privateKeyFilename, certificateFilename) 85 86 case "gen-proof-identity", "proof": 87 publicKeyFilename := getFilenameWithDirectory(arguments, proofPublicKeyFilename) 88 privateKeyFilename := getFilenameWithDirectory(arguments, proofPrivateKeyFilename) 89 err := zmqutil.MakeKeyPair(publicKeyFilename, privateKeyFilename) 90 if nil != err { 91 fmt.Printf("generate private key: %q and public key: %q error: %s\n", privateKeyFilename, publicKeyFilename, err) 92 exitwithstatus.Exit(1) 93 } 94 95 liveSigningKeyFilename := getFilenameWithDirectory(arguments, proofLiveSigningKeyFilename) 96 testSigningKeyFilename := getFilenameWithDirectory(arguments, proofTestSigningKeyFilename) 97 98 if err := makeSigningKey(false, liveSigningKeyFilename); nil != err { 99 fmt.Printf("generate the signing key for livenet: %q error: %s\n", liveSigningKeyFilename, err) 100 goto signing_key_failed 101 } 102 if err := makeSigningKey(true, testSigningKeyFilename); nil != err { 103 fmt.Printf(" generate the signing key for testnet: %q error: %s\n", testSigningKeyFilename, err) 104 goto signing_key_failed 105 } 106 107 fmt.Printf("generated private key: %q and public key: %q\n", privateKeyFilename, publicKeyFilename) 108 fmt.Printf("generated signing keys: %q and %q\n", liveSigningKeyFilename, testSigningKeyFilename) 109 return true 110 111 signing_key_failed: 112 _ = os.Remove(publicKeyFilename) 113 _ = os.Remove(privateKeyFilename) 114 _ = os.Remove(liveSigningKeyFilename) 115 _ = os.Remove(testSigningKeyFilename) 116 exitwithstatus.Exit(1) 117 118 case "dns-txt", "txt": 119 return false // defer processing until configuration is read 120 121 case "start", "run": 122 return false // continue processing 123 124 case "block", "b", "save-blocks", "save", "load-blocks", "load", "delete-down", "dd": 125 return false // defer processing until database is loaded 126 127 case "config-test", "cfg": 128 return false 129 130 case "version", "v": 131 fmt.Printf("%s\n", version) 132 return true 133 134 default: 135 switch command { 136 case "help", "h", "?": 137 case "", " ": 138 fmt.Printf("error: missing command\n") 139 default: 140 fmt.Printf("error: no such command: %q\n", command) 141 } 142 fmt.Printf("usage: %s [--help] [--verbose] [--quiet] --config-file=FILE [[command|help] arguments...]", program) 143 144 fmt.Printf("supported commands:\n\n") 145 fmt.Printf(" help (h) - display this message\n\n") 146 fmt.Printf(" version (v) - display version sting\n\n") 147 148 fmt.Printf(" gen-peer-identity [DIR] (peer) - create private key in: %q\n", "DIR/"+peerPrivateKeyFilename) 149 fmt.Printf(" and the public key in: %q\n", "DIR/"+peerPublicKeyFilename) 150 fmt.Printf("\n") 151 152 fmt.Printf(" gen-rpc-cert [DIR] (rpc) - create private key in: %q\n", "DIR/"+rpcPrivateKeyFilename) 153 fmt.Printf(" and the certificate in: %q\n", "DIR/"+rpcCertificateKeyFilename) 154 fmt.Printf("\n") 155 156 fmt.Printf(" gen-rpc-cert [DIR] [IPs...] - create private key in: %q\n", "DIR/"+rpcPrivateKeyFilename) 157 fmt.Printf(" and the certificate in: %q\n", "DIR/"+rpcCertificateKeyFilename) 158 fmt.Printf("\n") 159 160 fmt.Printf(" gen-proof-identity [DIR] (proof) - create private key in: %q\n", "DIR/"+proofPrivateKeyFilename) 161 fmt.Printf(" the public key in: %q\n", "DIR/"+proofPublicKeyFilename) 162 fmt.Printf(" and signing keys in: %q and: %q\n", "DIR/"+proofLiveSigningKeyFilename, "DIR/"+proofTestSigningKeyFilename) 163 fmt.Printf("\n") 164 165 fmt.Printf(" dns-txt (txt) - display the data to put in a dbs TXT record\n") 166 fmt.Printf("\n") 167 168 fmt.Printf(" start (run) - just run the program, same as no arguments\n") 169 fmt.Printf(" for convienience when passing script arguments\n") 170 fmt.Printf("\n") 171 172 fmt.Printf(" config-test (cfg) - just check the configuration file\n") 173 fmt.Printf("\n") 174 175 fmt.Printf(" block S [E [FILE]] (b) - dump block(s) as a JSON structures to stdout/file\n") 176 fmt.Printf("\n") 177 178 fmt.Printf(" save-blocks FILE (save) - dump all blocks to a file\n") 179 fmt.Printf("\n") 180 181 fmt.Printf(" load-blocks FILE (load) - restore all blocks from a file\n") 182 fmt.Printf(" only runs if database is deleted first\n") 183 fmt.Printf("\n") 184 185 fmt.Printf(" delete-down NUMBER (dd) - delete blocks in descending order\n") 186 fmt.Printf("\n") 187 188 exitwithstatus.Exit(1) 189 } 190 191 // indicate processing complete and preform normal exit from main 192 return true 193} 194 195// configuration file enquiry commands 196// have configuration file read and decoded, but nothing else 197func processConfigCommand(arguments []string, options *Configuration) bool { 198 199 command := "help" 200 if len(arguments) > 0 { 201 command = arguments[0] 202 } 203 204 switch command { 205 case "dns-txt", "txt": 206 dnsTXT(options) 207 208 case "config-test", "cfg": 209 b, err := json.Marshal(options) 210 if err != nil { 211 exitwithstatus.Message("error: %s", err) 212 } 213 var out bytes.Buffer 214 json.Indent(&out, b, "", " ") 215 out.WriteTo(os.Stdout) 216 os.Stdout.WriteString("\n") 217 218 default: // unknown commands fall through to data command 219 return false 220 } 221 222 // indicate processing complete and perform normal exit from main 223 return true 224} 225 226// data command handler 227// the internal block and transaction pools are enabled so these commands can 228// access and/or change these databases 229func processDataCommand(log *logger.L, arguments []string, options *Configuration) bool { 230 231 command := "help" 232 if len(arguments) > 0 { 233 command = arguments[0] 234 arguments = arguments[1:] 235 } 236 237 switch command { 238 239 case "start", "run": 240 return false // continue processing 241 242 case "block", "b": 243 if len(arguments) < 1 { 244 exitwithstatus.Message("missing block number argument") 245 } 246 247 n, err := strconv.ParseUint(arguments[0], 10, 64) 248 if nil != err { 249 exitwithstatus.Message("error in block number: %s", err) 250 } 251 if n < 2 { 252 exitwithstatus.Message("error: invalid block number: %d must be greater than 1", n) 253 } 254 255 output := "-" 256 257 // optional end range 258 nEnd := n 259 if len(arguments) > 1 { 260 261 nEnd, err = strconv.ParseUint(arguments[1], 10, 64) 262 if nil != err { 263 exitwithstatus.Message("error in ending block number: %s", err) 264 } 265 if nEnd < n { 266 exitwithstatus.Message("error: invalid ending block number: %d must be greater than 1", n) 267 } 268 } 269 270 if len(arguments) > 2 { 271 output = strings.TrimSpace(arguments[2]) 272 } 273 fd := os.Stdout 274 275 if output != "" && output != "-" { 276 fd, err = os.Create(output) 277 if nil != err { 278 exitwithstatus.Message("error: creating: %q error: %s", output, err) 279 } 280 } 281 282 fmt.Fprintf(fd, "[\n") 283 for ; n <= nEnd; n += 1 { 284 block, err := dumpBlock(n) 285 if nil != err { 286 exitwithstatus.Message("dump block error: %s", err) 287 } 288 s, err := json.MarshalIndent(block, " ", " ") 289 if nil != err { 290 exitwithstatus.Message("dump block JSON error: %s", err) 291 } 292 293 fmt.Fprintf(fd, " %s,\n", s) 294 } 295 fmt.Fprintf(fd, "{}]\n") 296 fd.Close() 297 298 case "save-blocks", "save": 299 if len(arguments) < 1 { 300 exitwithstatus.Message("missing file name argument") 301 } 302 filename := arguments[0] 303 if "" == filename { 304 exitwithstatus.Message("missing file name") 305 } 306 err := saveBinaryBlocks(filename) 307 if nil != err { 308 exitwithstatus.Message("failed writing: %q error: %s", filename, err) 309 } 310 311 case "load-blocks", "load": 312 if len(arguments) < 1 { 313 exitwithstatus.Message("missing file name argument") 314 } 315 filename := arguments[0] 316 if "" == filename { 317 exitwithstatus.Message("missing file name") 318 } 319 err := restoreBinaryBlocks(filename) 320 if nil != err { 321 exitwithstatus.Message("failed writing: %q error: %s", filename, err) 322 } 323 324 case "delete-down", "dd": 325 // delete blocks down to a given block number 326 if len(arguments) < 1 { 327 exitwithstatus.Message("missing block number argument") 328 } 329 330 n, err := strconv.ParseUint(arguments[0], 10, 64) 331 if nil != err { 332 exitwithstatus.Message("error in block number: %s", err) 333 } 334 if n < 2 { 335 exitwithstatus.Message("error: invalid block number: %d must be greater than 1", n) 336 } 337 err = block.DeleteDownToBlock(n) 338 if nil != err { 339 exitwithstatus.Message("block delete error: %s", err) 340 } 341 fmt.Printf("reduced height to: %d\n", blockheader.Height()) 342 343 default: 344 exitwithstatus.Message("error: no such command: %s", command) 345 346 } 347 348 // indicate processing complete and perform normal exit from main 349 return true 350} 351 352// print out the DNS TXT record 353func dnsTXT(options *Configuration) { 354 // <TAG> a=<IPv4;IPv6> c=<PEER-PORT> r=<RPC-PORT> f=<SHA3-256(cert)> p=<PUBLIC-KEY> 355 const txtRecord = `TXT "bitmark=v3 a=%s c=%d r=%d f=%x p=%x"` + "\n" 356 357 rpc := options.ClientRPC 358 359 keypair, err := tls.X509KeyPair([]byte(rpc.Certificate), []byte(rpc.PrivateKey)) 360 if nil != err { 361 exitwithstatus.Message("error: cannot decode certificate: %q error: %s", rpc.Certificate, err) 362 } 363 364 fingerprint := CertificateFingerprint(keypair.Certificate[0]) 365 366 if 0 == len(rpc.Announce) { 367 exitwithstatus.Message("error: no rpc announce fields given") 368 } 369 370 rpcIP4, rpcIP6, rpcPort := getFirstConnections(rpc.Announce) 371 if 0 == rpcPort { 372 exitwithstatus.Message("error: cannot determine rpc port") 373 } 374 375 peering := options.Peering 376 377 publicKey, err := zmqutil.ReadPublicKey(peering.PublicKey) 378 if nil != err { 379 exitwithstatus.Message("error: cannot read public key: %q error: %s", peering.PublicKey, err) 380 } 381 382 if 0 == len(peering.Announce) { 383 exitwithstatus.Message("error: no rpc announce fields given") 384 } 385 386 listenIP4, listenIP6, listenPort := getFirstConnections(peering.Announce) 387 if 0 == listenPort { 388 exitwithstatus.Message("error: cannot determine listen port") 389 } 390 391 IPs := "" 392 if "" != rpcIP4 && rpcIP4 == listenIP4 { 393 IPs = rpcIP4 394 } 395 if "" != rpcIP6 && rpcIP6 == listenIP6 { 396 if "" == IPs { 397 IPs = rpcIP6 398 } else { 399 IPs += ";" + rpcIP6 400 } 401 } 402 403 fmt.Printf("rpc fingerprint: %x\n", fingerprint) 404 fmt.Printf("rpc port: %d\n", rpcPort) 405 fmt.Printf("public key: %x\n", publicKey) 406 fmt.Printf("connect port: %d\n", listenPort) 407 fmt.Printf("IP4 IP6: %s\n", IPs) 408 409 fmt.Printf(txtRecord, IPs, listenPort, rpcPort, fingerprint, publicKey) 410} 411 412// extract first IP4 and/or IP6 connection 413func getFirstConnections(connections []string) (string, string, int) { 414 415 initialPort := 0 416 IP4 := "" 417 IP6 := "" 418 419scan_connections: 420 for i, c := range connections { 421 if "" == c { 422 continue scan_connections 423 } 424 v6, IP, port, err := splitConnection(c) 425 if nil != err { 426 exitwithstatus.Message("error: cannot decode[%d]: %q error: %s", i, c, err) 427 } 428 if v6 { 429 if "" == IP6 { 430 IP6 = IP 431 if 0 == initialPort || port == initialPort { 432 initialPort = port 433 } 434 } 435 } else { 436 if "" == IP4 { 437 IP4 = IP 438 if 0 == initialPort || port == initialPort { 439 initialPort = port 440 } 441 } 442 } 443 } 444 return IP4, IP6, initialPort 445} 446 447// split connection into ip and port 448func splitConnection(hostPort string) (bool, string, int, error) { 449 host, port, err := net.SplitHostPort(hostPort) 450 if nil != err { 451 return false, "", 0, fault.InvalidIpAddress 452 } 453 454 IP := net.ParseIP(strings.TrimSpace(host)) 455 if nil == IP { 456 return false, "", 0, fault.InvalidIpAddress 457 } 458 459 numericPort, err := strconv.Atoi(strings.TrimSpace(port)) 460 if nil != err { 461 return false, "", 0, err 462 } 463 if numericPort < 1 || numericPort > 65535 { 464 return false, "", 0, fault.InvalidPortNumber 465 } 466 467 if nil != IP.To4() { 468 return false, IP.String(), numericPort, nil 469 } 470 return true, "[" + IP.String() + "]", numericPort, nil 471} 472 473// get the working directory; if not set in the arguments 474// it's set to the current directory 475func getFilenameWithDirectory(arguments []string, name string) string { 476 dir := "." 477 if len(arguments) >= 1 { 478 dir = arguments[0] 479 } 480 481 return filepath.Join(dir, name) 482} 483 484func makeSigningKey(testnet bool, fileName string) error { 485 seed, err := account.NewBase58EncodedSeedV2(testnet) 486 if nil != err { 487 return err 488 } 489 490 data := "SEED:" + seed + "\n" 491 if err = ioutil.WriteFile(fileName, []byte(data), 0600); nil != err { 492 return fmt.Errorf("error writing signing key file error: %s", err) 493 } 494 495 return nil 496} 497